Why your OpenCode plugin or hook isn't running
A plugin is a JavaScript or TypeScript module that OpenCode loads to add tools, commands, or hooks. A hook is a function the plugin registers for a point in OpenCode's lifecycle: before a tool runs, after a tool runs, when a session starts, when a prompt arrives. The plugin can be present, the hook spelled correctly, and it still does nothing: "not running" covers four failures — not loaded, wrong surface, hook not registered, hook not fired. Start at the top.
OpenCode 1.x and 2.x use different plugin APIs and config keys, and a plugin written for one does not run on the other. Run opencode --version: a 1.x version is V1, a 2.x version is V2. This page leads with V2 and labels the V1 checks.
Checked against OpenCode's documentation on 27 October 2026.
Status, 27 October 2026: the V1-to-V2 boundary is the first thing to check. The V2 migration guide says V1 plugin implementations do not run in V2, and that moving a file or renaming its config entry is not enough. On the 1.x line, the permission.ask plugin hook still does not fire; the report was closed on 1 October 2026 because V2 replaced it with permission.evaluate (#7006). Check those two first; they explain many upgrade failures.
Which OpenCode are you on?#
| Item | V1 (1.x) | V2 (2.x) |
|---|---|---|
| Config key | plugin | plugins |
| Plugin shape | A function that returns an object of hooks | A definition with an id and setup(ctx) that registers hooks |
| Local plugin files | .opencode/plugins/, ~/.config/opencode/plugins/ | The same directories, plus the singular .opencode/plugin/ and direct .ts and .js files |
The V2 migration guide is blunt: "V1 plugin implementations do not run in V2." A package can expose both entrypoints, but the two APIs remain separate and hooks are not translated. If you moved a V1 file into a V2 project and changed plugin to plugins, you have configured a plugin that still cannot run. Port the implementation or use a version of the package that supports V2.
A V2 plugin also needs a stable id for status and diagnostics; leave it set even for a local plugin.
1. Did the server load the plugin?#
On V2 the plugin runs in the OpenCode server, not the terminal; the desktop app and IDE extensions are clients too. --standalone runs a private server, and --server points a client at a specific one; the plugin must be on the server the client uses.
Check what the server has:
opencode plugin list
opencode plugin list --builtin
opencode debug config
opencode service statusplugin list shows ID, version, and source; --builtin adds the built-ins. debug config lists the merged config sources, so you can see where your plugins entry came from. service status checks the background server; if it is stuck, run opencode service restart. On V1, the plugin docs point to opencode debug info: it shows the version, OS, terminal, and configured plugins. "Configured" is not "loaded", but a missing entry suggests the process read a different config file.
The paths that do not load#
V2 is stricter about local paths. A configured path must be a directory; a single file is skipped with a warning. ./plugins/my-plugin.ts is wrong; ./plugins/my-plugin/index.ts is right. A plugins/ directory beside a project-root opencode.json is not discovered; put it under .opencode/ or list it. Direct .ts and .js files under a discovered .opencode/plugins/ load, as do package directories with a package.json. V2 also discovers the singular .opencode/plugin/; use the plural for new files. Global plugins use ~/.config/opencode/plugins/. V1 expects files directly in those directories; if one never appears, check the lazy-load note below.
The install can fail before the plugin loads#
An npm plugin installs into a cache. V1 documents ~/.cache/opencode/node_modules/ and installs with Bun at startup. V2 loads cached packages immediately and installs missing ones in the background:
opencode plugin add [email protected]
opencode plugin listIf plugin add fails, the plugin is not there to load. A V1 proxy bug made configured npm plugins disappear while OpenCode started normally; the workaround was a local install pointed at by config (#21098). It closed on 20 April 2026 after a maintainer described an installer patch and told users to clear the package cache; no release is named, so older versions still need the local-directory workaround.
Load order and duplicates#
V1 loads the global config, project config, global plugin directory, then project plugin directory; hooks run in sequence. V2 merges plugin arrays by precedence and processes entries in order; - disables one and * selects all. V1's isolation flag is --pure (or OPENCODE_PURE=1); V2's CLI help does not list it.
A plugin you edited may not be the copy running: duplicate entries or a stale server can leave old code in place. V2 reloads watched config; unwatched local dependencies may need opencode service restart. On V1, restart OpenCode.
2. Is it the right surface?#
V2 splits a plugin into parts. The server part adds tools, hooks, commands, agents, and integrations and runs in the OpenCode server. The TUI part adds terminal commands, routes, and renderers. The GUI part adds desktop panels, commands, and settings.
One entry loads every part. The TUI loads the TUI parts of its server's plugins. The desktop app loads the GUI parts from the server it runs, so configure the plugin in your global opencode.jsonc to use it in every window. A package with only a TUI part goes in cli.json, not opencode.json.
A TUI-only plugin in cli.json remains active when the TUI is connected to a remote server, but it is not a server plugin and does not add tools to the model. A GUI plugin in a project config may add its server behavior while its desktop panel is missing. The desktop app's plugin switch turns off only the desktop UI; to unload the whole plugin, add - and its ID to plugins.
If the desktop app has no plugins while opencode plugin list does, check which server it uses. Settings > Extensions > Plugins lists plugins only for the app's own server; a remote or stale server is a different list.
3. Did the hook register?#
A plugin can load and still register nothing useful. V2 registers hooks on the domain that owns the operation, and each callback receives one mutable event, not separate input and output objects.
| V1 hook | V2 API |
|---|---|
tool.execute.before | ctx.tool.hook("execute.before", ...) |
tool.execute.after | ctx.tool.hook("execute.after", ...) |
permission.ask | ctx.permission.hook("evaluate", ...) |
These are destinations, not exact renames. In particular, prompt runs before prompt admission, while context runs immediately before a model request; register context, compaction, generate, and title separately for every request kind.
Hooks that are not supposed to fire#
Some "dead hooks" are by design. server.connected never reaches plugins: they run inside the server. The V2 prompt hook does not run for synthetic messages, shell messages, compaction controls, or move controls; it runs once, when a prompt is admitted. On V1, permission.ask never fires; V2 replaced it with permission.evaluate. And a hook can be registered but never triggered by the path you use: a maintainer test found that subagent tool calls do fire tool.execute.before, while a later report traced a real bypass to the batch tool (#5894, closed 15 April 2026). Test the path in front of you.
If the rule must hold no matter which tool path runs, do not rely on a hook alone. OpenCode's permission rules are evaluated by the server; a plugin hook is code that may not be on every path. The permission guide covers that layer.
4. Did it fire for this path?#
Two more things can make a registered hook look absent. In V2, ctx.location is where that plugin instance loaded, not every session it can observe, so a session in another directory or project may be served by a different instance. And the process may be old: V2 reloads watched config, but a local dependency can be cached and a shared server can outlive the edit. opencode service restart loads the new code; on V1, restart OpenCode. A plugin that disappeared after a 1.14.31 to 1.14.32 upgrade needed 1.14.33 (#25441, closed 3 May 2026).
On 1.17.x, plugin loading became lazy: plugins initialize on the first hook trigger, not at startup, so a DEBUG log with no plugin lines is not proof the plugin is missing (#33455, closed 22 June 2026). Run a harmless tool, then check the log.
Prove it with a probe#
Do not ask the model whether its plugin loaded. It cannot see the registry and will guess. Write a probe that leaves a line in the server log.
For V2, a direct .ts file under .opencode/plugins/:
import { Plugin } from "@opencode/plugin"
export default Plugin.define({
id: "probe.loaded",
async setup(ctx) {
console.log("probe loaded", ctx.app.version)
await ctx.tool.hook("execute.before", (event) => {
console.log("probe tool", event.tool)
})
},
})For V1:
import type { Plugin } from "@opencode-ai/plugin"
export const Probe: Plugin = async ({ client }) => {
await client.app.log({
body: { service: "probe", level: "info", message: "probe loaded" },
})
return {
"tool.execute.before": async (input) => {
await client.app.log({
body: { service: "probe", level: "info", message: `tool ${input.tool}` },
})
},
}
}Then watch the server log while you exercise a real tool:
tail -f ~/.local/share/opencode/log/opencode.log
# V2: plugin and tool activity is in the server role
grep 'role=server' ~/.local/share/opencode/log/opencode.log
# V1: look for the probe's own service line
grep 'service=probe' ~/.local/share/opencode/log/opencode.logOn Windows, the log is under %USERPROFILE%\.local\share\opencode\log. You want two lines: the probe's setup line and the tool line. If the first is missing, the problem is loading; if the second is missing, registration, the event, or the path. On builds before 1.17.x, the runtime's load line is service=plugin; on 1.17+ lazy loading means it may be absent, so the probe writes service=probe.
Where to go next#
- OpenCode guides — the hub for the set.
- Why OpenCode permission rules don't fire — the enforced layer.
- Why OpenCode ignores your AGENTS.md — if the plugin loaded but its instructions did not.
- What /undo really reverts in OpenCode — when a plugin changed files.