opencode-skill-autodiscovery
v2.0.0
Published
OpenCode plugin that auto-discovers skills installed by VS Code agent plugins, Claude Code plugins, and Agent Plugins 1.0.0 packages, and registers them with the local opencode config.
Maintainers
Readme
opencode-skill-autodiscovery
An opencode plugin that auto-discovers skills installed by
VS Code agent plugins, Claude Code plugins, and Agent Plugins
1.0.0 conformant packages on the current machine,
and registers them with opencode's skills.paths so they appear in every
session. Each discovered skill is also exposed as a /skill-name slash command
that loads the skill and routes the rest of your message through it.
Why
Skills live in different places depending on the tool, OS, and local vs remote setup:
- VS Code agent plugins, local client:
~/.vscode/agent-plugins(Windows / older builds),~/.config/Code/agentPlugins(Linux),~/Library/Application Support/Code/agentPlugins(macOS), and%APPDATA%\Code\agentPlugins(Windows), plusCode - Insidersvariants. The install layout is{host}/{org}/{repo}, with optionalinstalled.jsonandcache.jsonmanifests alongside. - VS Code agent plugins on a remote host (Remote-SSH, Codespaces, Dev
Containers, WSL):
~/.vscode-server/data/agentPlugins. VS Code syncs the enabled skills from your local client into a synthetic "VS Code Synced Data" plugin materialized at~/.vscode-server/data/agentPlugins/{sanitizedUri}/{nonce}/skills/..., and records each synced bundle in~/.vscode-server/data/agentPlugins/cache.json. - Claude Code plugins:
~/.claude/plugins/installed_plugins.json - Claude Code plugins on a remote host (SSH/remote sessions):
~/.claude/remote/plugins/installed_plugins.json - Agent Plugins 1.0.0 packages distributed via npm. opencode installs npm
plugins into its own cache (
~/.cache/opencode/packages/...), not the project'snode_modules, and askills.pathsentry relative to the project dir silently resolves to nothing. This plugin scans both the opencode plugin cache and the project'snode_modulesfor packages carrying a rootplugin.jsonwhose$schemaishttps://agent-plugins.org/schemas/..., and registers theirskills/with absolute paths — so an npm-distributed Agent Plugins package (e.g.@dodopayments/opencode-plugin) just works with"plugin": ["opencode-skill-autodiscovery"]and nothing else.
Each points at plugin directories that contain SKILL.md files. This plugin
reads the manifests (including the remote cache.json LRU), resolves each
entry to its on-disk skill directories, and registers them with opencode.
When a discovered package carries a conformant root plugin.json, that manifest
is preferred (its skills/ children are registered as-is); otherwise the plugin
falls back to a tree walk so legacy layouts keep working.
MCP servers (opt-in)
With the mcp option, the plugin also reads each discovered package's mcp.json
and maps it onto opencode's config.mcp:
| Agent Plugins server | opencode entry |
|---|---|
| { "type": "stdio", "command", "args", "env" } | { "type": "local", "command": [...], "environment": {...} } |
| { "type": "streamable-http", "url" } | { "type": "remote", "url" } |
| { "type": "sse" } | skipped (opencode has no SSE transport) |
${PLUGIN_ROOT} and ${PLUGIN_DATA} placeholders are expanded in args/env,
and both variables are injected into each stdio server's environment
(PLUGIN_DATA points at
{opencode state}/plugin-data/{packageName}). Invalid entries are skipped
per-entry, never fatally. sse servers and stdio cwd (which opencode cannot
represent) are dropped with a log line.
Agents (opt-in)
Agent Plugins 1.0.0 has no portable "agents" component type (only skills and
MCP servers), so agents are contributed through the spec's client-extension
mechanism. With the agents option, the plugin reads agents from four
sources, in order:
plugin.json→extensions["dev.opencode"].agents(a map of agent name → opencodeagentconfig).- A
dev.opencode/agents/<name>.jsonextension directory (one opencode agent config per file). - A legacy Claude Code plugin shim:
.claude-plugin/plugin.jsonagents, mappingdescriptionandsystemPrompt(or theagents/<name>/AGENTS.mdbody) onto an opencode agent. - Flat
agents/<name>.mdfiles (the agency-agents layout) — one agent per file, keyed by filename, withdescription/colorfrom frontmatter and the markdown body as the prompt.
Agents discovered without an explicit mode default to opencode's all, so
they are both Tab-selectable and spawnable as subagents.
Discovered agents are registered as config.agent.<name> with the same rules
as commands: user-defined agents are never overwritten, same-source mirrors are
collapsed, and cross-source collisions become <package>-<agent>.
Trust note: a package-supplied permission block is always dropped — agent
permissions are too powerful to inherit from a package by default. If you need
one, define the agent yourself in opencode.json (which always wins).
Slash commands
opencode treats skills and slash commands as separate mechanisms: skills are
only loaded on demand via the skill tool. To make a discovered skill
invocable as /name, the plugin also registers a command for it (via
config.command) whose template loads the skill and forwards your arguments:
Load the "spec" skill and follow its instructions.
Context: $ARGUMENTSThe skill name is rendered only as a quoted data value in the template — never inside backticks — so a hostile name cannot break out into surrounding instruction text.
So /spec plan the migration loads the spec skill and runs it against
plan the migration. The command's description is taken from the skill's
frontmatter; existing commands with the same name are never overwritten (a
colliding skill from a different source is registered as /package-skill).
Install
Add the package name to the plugin array in your opencode.json:
{
"plugin": ["opencode-skill-autodiscovery"]
}Use the tuple form to configure options:
{
"plugin": [
[
"opencode-skill-autodiscovery",
{
"extraRoots": ["/home/user/.vscode-server"],
"scanCache": true,
"scanNodeModules": true,
"exclude": ["unwanted-plugin"],
"mcp": false,
"agents": false
}
]
]
}| Option | Default | Meaning |
|---|---|---|
| extraRoots | [] | Extra root directories to scan (e.g. a non-standard VS Code data location on a remote host). |
| scanCache | false | Scan opencode's plugin cache (~/.cache/opencode/packages/*) for Agent Plugins packages. |
| scanNodeModules | false | Scan the project's node_modules (incl. @scope/*) for Agent Plugins packages. |
| exclude | [] | Package names to skip during discovery, regardless of trust tier. Matches the conformant package's plugin.json name, or the directory basename when there is no manifest. |
| mcp | false | Also register MCP servers from discovered packages' mcp.json. |
| agents | false | Also register agents from packages (see "Agents" above). |
Both scan flags default to false for supply-chain reasons: a discovered
skill's SKILL.md becomes prompt material in your sessions, so anything that
plants a skill plants model-facing instructions — and neither transitive npm
dependencies (which land in node_modules without any install script running)
nor the shared ~/.cache/opencode/packages directory (populated by every
project on the machine) requires a manifest you ever reviewed. This is the
OWASP LLM01 risk arriving via the software supply chain; both sources are
therefore opt-in, while manifest-mediated sources (Claude Code plugins, VS
Code agent plugins) stay default-on because their manifests record deliberate,
host-vouched installs.
Threat model
Discovery reads manifests from well-known locations and registers what it finds into your session config. Not all sources are equally trustworthy, so they are split into two tiers:
Trusted by default — content a host tool or opencode itself installed deliberately, vouched for by a manifest:
- Claude Code plugins (
installed_plugins.json, local and remote) - VS Code agent plugins recorded in
installed.json/cache.json - packages in opencode's own plugin cache (
~/.cache/opencode/packages/*), which exist because your config asked opencode to fetch them
Untrusted by default — content present merely as a side effect:
- the project's
node_modules: dependencies install transitively, so anything in the tree can ship skills, MCP servers, or agents. Scanning it is off by default; restore it explicitly with"scanNodeModules": true. - user-supplied
extraRoots: the plugin cannot vouch for whatever you point it at. - manifest-less directory walks (e.g. cloned-but-uninstalled marketplace folders).
Trust decides whether content gets registered, not whether it is safe: a
registered skill's SKILL.md becomes prompt material in your sessions. The
$schema check identifies format only — never provenance or safety. Any package
can copy the literal schema URL, so a conformant manifest proves nothing about
who wrote it.
mcp and agents trust everything they find
Both flags are single global switches: enabling one trusts every discovered package that ships the matching config. There is no per-package consent step.
mcp: trueregisters every conformant package'smcp.json; stdio entries execute commands on your machine.agents: trueregisters package-supplied agents essentially verbatim within the schema: a package can setmode: "primary"(making itself a primary agent) and arbitrarytoolsbooleans such as"write": true. Onlypermissionblocks are dropped.
Pair these flags with exclude to carve out packages you do not want
registered:
{
"plugin": [
["opencode-skill-autodiscovery", { "mcp": true, "agents": true, "exclude": ["unwanted-package"] }]
]
}Residual risk, stated plainly: approving or rejecting an individual package's
MCP servers or agents would require an interactive consent surface, which
opencode's synchronous config hook cannot provide. The granularity available
today is all-or-nothing per component type, narrowed by exclude.
Identifier rules
Every package-supplied identifier that becomes a config key must match the
same pattern: lowercase letters, digits, -, and . only ([a-z0-9.-]),
starting and ending with an alphanumeric character, with no -- or ..
runs. The prototype-chain keys __proto__ and constructor are rejected
outright even though they satisfy the character pattern. This applies to:
- the
plugin.jsonmanifestname(collision namespaces andexcludematching) SKILL.mdfrontmattername(slash-command keys)mcp.jsonserver keys (config.mcpkeys)- agent manifest keys,
dev.opencode/agents/*.jsonfilenames, legacy shim agent names, and flatagents/<name>.mdfilenames (config.agentkeys)
An invalid identifier never aborts discovery: the offending entry is skipped with a log line naming the package, its source, and a reason, and legitimate entries from the same package still register.
Defense-in-depth at the write sites holds even if a future call site skips those checks:
- The
config.command,config.mcp, andconfig.agentcontainers are built prototype-free (Object.create(null)), so no key can ever resolve to an inherited member such astoString, and__proto__/constructorcan never take effect through inheritance. - The skill name inside a slash-command template is rendered as a quoted data value (JSON string encoding), never wrapped in backticks, so it cannot break out into surrounding instruction text.
- Frontmatter
descriptionstrings pass through the same control-character sanitizer used for log lines before they enter config.
Migrating from 1.x
scanNodeModules used to default to true; it now defaults to false. If you
distribute an Agent Plugins package via npm and consumers relied on it being
picked up from the project's node_modules, they must now opt in explicitly:
{
"plugin": [["opencode-skill-autodiscovery", { "scanNodeModules": true }]]
}Prefer shipping your package as a regular opencode plugin
("plugin": ["your-package"]) instead: opencode installs it into its own cache,
where you can pick it up explicitly with "scanCache": true — no
scan of the dependency tree required.
VPS / remote hosts (SSH sessions)
Discovery is machine-local: a remote session only sees the skills, agents, and packages installed on that host. So opencode (and this plugin) must be installed on each remote machine, and you run opencode inside the SSH session — not from a local client terminal.
On each remote host:
# 1. Install opencode if it isn't there yet
npm install -g opencode-ai
# 2. Install this plugin globally. The -g flag targets the machine-wide config;
# without it, opencode writes a project-scoped .opencode/opencode.json
# instead (easy to miss, because the plugin looks installed but only for
# that one directory).
opencode plugin opencode-skill-autodiscovery -g
# 3. Restart opencode. It fetches the latest published version into the
# remote's own cache.Or hand-edit the remote's global config (~/.config/opencode/opencode.json):
{
"plugin": [["opencode-skill-autodiscovery", { "mcp": true, "agents": true }]]
}What a remote session discovers (that host's own installs):
- VS Code Remote-SSH synced skills from
~/.vscode-server/data/agentPlugins/(read via thecache.jsonLRU). - Claude Code remote plugins from
~/.claude/remote/plugins/. - Agent Plugins packages in the remote's opencode cache
(
~/.cache/opencode/packages/*) and the project'snode_modules.
Caveats:
- Run opencode on the remote. A local client terminal reads the local machine's manifests, not the remote's.
- VS Code only syncs enabled skills to the server as a flattened "VS Code
Synced Data" bundle; marketplace clones (including their
agents/*.mdfiles) are generally not copied. Install the pack on the remote too if you want its agents there. - To force a re-fetch of a new release on a remote, clear the cached copy and
restart (opencode re-downloads the latest):
rm -rf ~/.cache/opencode/packages/opencode-skill-autodiscovery*
Notes
- Discovery is inherently machine-local: it reads manifests from the home directory of the machine opencode is running on. Remote sessions on a different machine will discover that machine's skills.
- On a remote host, VS Code does not copy your marketplace plugins over.
It syncs only the enabled skills/agents/etc. from your client as a single
flattened "VS Code Synced Data" bundle under
~/.vscode-server/data/agentPlugins/. That is why you won't see the original{org}/{repo}layout on the remote — the skill directories are named after the skills instead. This plugin readscache.jsonto locate those materialized bundles. - Newer VS Code layouts have no
installed.jsonat all; marketplaces are cloned directly under the agent plugin dir. The plugin falls back to a tree walk only when no manifest is present, so it never surfaces skills from cloned-but-uninstalled marketplaces on setups that do have a manifest. - VS Code account sync only syncs your marketplace extension list; each machine
still has its own
installed.json/cache.jsonthat this plugin reads. - A package is only treated as an Agent Plugins package when its root
plugin.jsondeclares a$schemaunderhttps://agent-plugins.org/schemas/. Everything else is ignored by the cache/node_modules scanners and falls back to the tree walk elsewhere. This check identifies format only — never provenance or safety: any package can copy the literal schema URL, so a conformant manifest does not make scanning an untrusted directory safe. Content is trusted based on where it came from (host-installed manifests vs. side-effect locations), not on its shape. - The same plugin can be materialised in several VS Code layouts at once
(local clone + synced bundle + marketplace clone).
skills.pathskeeps all paths; slash commands and MCP servers are de-duplicated so mirrors don't create a wall of/mirror-of-...junk. - The plugin is a no-op when no manifests or conformant packages exist, or when they contain no skills.
Development
npm install
npm run build # tsc -> dist/
npm test # build + node --test (fixture-based unit tests)Publish:
npm publish --access public