OpenCode Plugins Reference
Updated by Alex Sorokoletov
Independent reference, not affiliated with Anomaly. Tables are generated from the OpenCode source at v1.18.33 (v1.18.33). Official documentation: opencode.ai.
An OpenCode plugin is a JavaScript or TypeScript function that receives a context object and returns an object of hooks. OpenCode 1.x defines 21 hooks, including tool.execute.before, tool.execute.after, chat.params and an event hook that sees every bus event. Plugins load from the plugin array in opencode.json and from plugin/ or plugins/ folders.
Does OpenCode have hooks?#
Yes, as plugin functions rather than shell commands. Every hook below runs in the OpenCode process. The trigger-style hooks take (input, output): input describes the call, output is a mutable object, and changes made to output are what OpenCode uses. Plugins run one after another in load order on the same output object, so a later plugin sees earlier plugins' changes. "Called from" lists the OpenCode source files that invoke the hook in this release.
| Hook | Input | Output (mutable) | Called from | Notes |
|---|---|---|---|---|
dispose | — | — | plugin/index.ts | — |
event | { event: Event } | — | plugin/index.ts | — |
config | Config | — | plugin/index.ts | — |
tool | { [key: string]: ToolDefinition } | — | tool/registry.ts | — |
auth | AuthHook | — | provider/auth.ts, provider/provider.ts | — |
provider | ProviderHook | — | provider/provider.ts | — |
chat.message | { sessionID: string; agent?: string; model?: { providerID: string; modelID: string }; messageID?: string; variant?: string } | { message: UserMessage; parts: Part[] } | session/prompt.ts | Called when a new message is received |
chat.params | { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage } | { temperature: number; topP: number; topK: number; maxOutputTokens: number | undefined; options: Record<string, any> } | session/llm/request.ts | Modify parameters sent to LLM |
chat.headers | { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage } | { headers: Record<string, string> } | session/llm/request.ts | — |
permission.ask | Permission | { status: "ask" | "deny" | "allow" } | never called in this release | — |
command.execute.before | { command: string; sessionID: string; arguments: string } | { parts: Part[] } | session/prompt.ts | — |
tool.execute.before | { tool: string; sessionID: string; callID: string } | { args: any } | session/prompt.ts, session/tools.ts, tool/code-mode.ts | — |
shell.env | { cwd: string; sessionID?: string; callID?: string } | { env: Record<string, string> } | plugin/pty-environment.ts, server/routes/instance/httpapi/handlers/pty.ts, session/prompt.ts, tool/shell.ts | — |
tool.execute.after | { tool: string; sessionID: string; callID: string; args: any } | { title: string; output: string; metadata: any } | session/prompt.ts, session/tools.ts, tool/code-mode.ts | — |
experimental.chat.messages.transform | {} | { messages: { info: Message; parts: Part[] }[] } | session/compaction.ts, session/prompt.ts | — |
experimental.chat.system.transform | { sessionID?: string; model: Model } | { system: string[] } | agent/agent.ts, session/llm/request.ts | — |
experimental.provider.small_model | { provider: ProviderV2 } | { model?: ModelV2 } | provider/provider.ts | — |
experimental.session.compacting | { sessionID: string } | { context: string[]; prompt?: string } | session/compaction.ts | Called before session compaction starts. Allows plugins to customize the compaction prompt. |
experimental.compaction.autocontinue | { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage; overflow: boolean } | { enabled: boolean } | session/compaction.ts | Called after compaction succeeds and before a synthetic user auto-continue message is added. |
experimental.text.complete | { sessionID: string; messageID: string; partID: string } | { text: string } | session/processor.ts | — |
tool.definition | { toolID: string } | { description: string; parameters: any } | tool/registry.ts | Modify tool definitions (description and parameters) sent to LLM |
permission.ask is declared in the plugin types but nothing in 1.18.33 invokes it, so a plugin cannot answer permission prompts through it. The event hook is called without being awaited, which makes it observe-only: use it for notifications and logging, and use the trigger hooks for anything that must change behaviour.
What does a plugin function receive?#
| Field | Type |
|---|---|
client | ReturnType<typeof createOpencodeClient> |
project | Project |
directory | string |
worktree | string |
experimental_workspace | { register(type: string, adapter: WorkspaceAdapter): void } |
serverUrl | URL |
$ | BunShell |
client is an SDK client already pointed at the running server, $ is Bun's shell (undefined when OpenCode runs outside Bun), and directory and worktree are the working directory and the project root. The second argument, options, is the object from a ["name", { ... }] entry in the config plugin array.
import type { Plugin } from "@opencode-ai/plugin"
export const EnvGuard: Plugin = async ({ directory }) => ({
"tool.execute.before": async (input, output) => {
if (input.tool === "read" && String(output.args.filePath).endsWith(".env")) {
throw new Error("Reading .env files is blocked")
}
},
})
A module can export plugin functions as named exports, or default-export { id, server } where server is the plugin function.
Where do OpenCode plugins go?#
| Load order | Source | Where | Notes |
|---|---|---|---|
| 1 | built-in plugins (12) | bundled (provider auth and model hooks) | skipped when OPENCODE_DISABLE_DEFAULT_PLUGINS=1 |
| 2 | `plugin` array in each config file | npm spec (`name`, `name@version`) or file path | paths resolve relative to the config file that lists them |
| 3 | `{plugin,plugins}/*.{ts,js}` in each config directory | ~/.config/opencode, every .opencode/ from the working directory up to the worktree root, ~/.opencode, $OPENCODE_CONFIG_DIR | files directly in the folder; subfolders are not scanned |
Config directories are ~/.config/opencode, each .opencode/ folder from the working directory up to the git worktree root, ~/.opencode, and $OPENCODE_CONFIG_DIR. Only .ts and .js files directly inside plugin/ or plugins/ load; .mjs files and subfolders are skipped. npm entries without a version install as @latest. OpenCode also runs a background install in each config directory that adds @opencode-ai/plugin, and writes a .gitignore covering node_modules, package.json and lock files where none exists.
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-wakatime", ["./tools/my-plugin.ts", { "verbose": true }]]
}
Why is my OpenCode plugin not loading?#
| Situation | What OpenCode does |
|---|---|
| `plugins` key (plural) in opencode.json | ignored with a compatibility warning; the v1 key is `plugin` |
| same package listed in several config files | loaded once; the last config file to list it wins |
| npm plugin with `engines.opencode` that excludes this version | skipped: "Plugin requires opencode <range> but running <version>" |
| npm install fails | skipped: "Failed to install plugin <pkg>@<version>" shown as a session error |
| module exports a non-function (legacy function-export style) | whole file rejected: "Plugin export is not a function" |
| default export has both `server` and `tui` | rejected: a module exports one or the other |
| OPENCODE_PURE set | no external plugins load; built-ins still do |
| old auth packages (opencode-openai-codex-auth, opencode-copilot-auth) | silently ignored: now built in |
Install, compatibility and load failures surface as a session error naming the plugin. A plugin that throws during setup is logged as "failed to load plugin" and skipped without a visible error, so check the log when a local plugin silently does nothing. Unknown config keys never raise an error in this release: they are dropped when the file is parsed, which is why a misspelled plugins key looks like a plugin that never loaded.
How do OpenCode plugins compare to Claude Code hooks?#
Claude Code hooks are shell commands declared in settings.json that read JSON on stdin and block with exit code 2. OpenCode plugins are in-process functions with no matcher: filter on input.tool inside the hook. The closest equivalents:
- PreToolUse:
tool.execute.before. Throw to stop the call (the tool does not run), or change fields ofoutput.argsto rewrite its arguments (the tool runs with that same object, so assigning a new object tooutput.argshas no effect). MCP tools pass through the same hook with their<server>_<tool>name. - PostToolUse:
tool.execute.after, withoutput.title,output.outputandoutput.metadatawritable before the result reaches the model. - UserPromptSubmit:
chat.message, which can modify theoutput.partsarray of the incoming message in place. - PreCompact:
experimental.session.compacting, which appendscontextstrings or replaces the compactionprompt. - SessionStart, Stop, Notification: the
eventhook withsession.created,session.idleorpermission.asked, observe-only. - PermissionRequest: no working equivalent;
permission.askis never called.
Which events can the event hook receive?#
The event hook receives { event } with type and properties for every event on the bus for the plugin's directory. These are the event types in the server's event union:
| Group | Event types | Count |
|---|---|---|
models-dev | models-dev.refreshed | 1 |
integration | integration.updated, integration.connection.updated | 2 |
catalog | catalog.updated | 1 |
session | session.created, session.updated, session.deleted, session.diff, session.error, session.status, session.idle, session.compacted | 8 |
message | message.updated, message.removed, message.part.updated, message.part.removed, message.part.delta | 5 |
session.next | session.next.agent.switched, session.next.model.switched, session.next.moved, session.next.prompted, session.next.prompt.admitted, session.next.context.updated, session.next.synthetic, session.next.shell.started, session.next.shell.ended, session.next.step.started, session.next.step.ended, session.next.step.failed, session.next.text.started, session.next.text.delta, session.next.text.ended, session.next.reasoning.started, session.next.reasoning.delta, session.next.reasoning.ended, session.next.tool.input.started, session.next.tool.input.delta, session.next.tool.input.ended, session.next.tool.called, session.next.tool.progress, session.next.tool.success, session.next.tool.failed, session.next.retried, session.next.compaction.started, session.next.compaction.delta, session.next.compaction.ended, session.next.revert.staged, session.next.revert.cleared, session.next.revert.committed | 32 |
installation | installation.updated, installation.update-available | 2 |
file | file.edited, file.watcher.updated | 2 |
reference | reference.updated | 1 |
permission | permission.v2.asked, permission.v2.replied, permission.asked, permission.replied | 4 |
plugin | plugin.added | 1 |
project | project.directories.updated, project.updated | 2 |
pty | pty.created, pty.updated, pty.exited, pty.deleted | 4 |
question | question.v2.asked, question.v2.replied, question.v2.rejected, question.asked, question.replied, question.rejected | 6 |
todo | todo.updated | 1 |
lsp | lsp.updated | 1 |
tui | tui.prompt.append, tui.command.execute, tui.toast.show, tui.session.select | 4 |
mcp | mcp.tools.changed, mcp.browser.open.failed | 2 |
command | command.executed | 1 |
vcs | vcs.branch.updated | 1 |
workspace | workspace.ready, workspace.failed, workspace.status | 3 |
worktree | worktree.ready, worktree.failed | 2 |
server | server.connected, server.instance.disposed | 2 |
global | global.disposed | 1 |
Frequently asked questions#
Does OpenCode have hooks like Claude Code?#
Yes, through plugins. tool.execute.before and tool.execute.after cover PreToolUse and PostToolUse, chat.message covers UserPromptSubmit, and experimental.session.compacting covers PreCompact. They are TypeScript or JavaScript functions running inside OpenCode, not shell commands, so there is no exit code 2: a tool.execute.before hook blocks a call by throwing an error.
Where do OpenCode plugins go?#
Put a .ts or .js file in .opencode/plugin/ or .opencode/plugins/ for one project, or in ~/.config/opencode/plugin/ or ~/.config/opencode/plugins/ for every project. Files must sit directly in that folder. npm packages and file paths go in the plugin array of opencode.json, where relative paths resolve against the config file that lists them.
Why are my OpenCode plugins not loading?#
Check the key name first: 1.x reads plugin, and a plugins key is ignored with only a log warning. Other causes are a file in a subfolder, a .mjs extension, a module that also exports a non-function value, an npm package whose engines.opencode range excludes your version, or OPENCODE_PURE being set.
What does "unrecognized key plugins" mean in OpenCode?#
The config used the plural plugins key. OpenCode 1.x expects the singular plugin; plugins is the OpenCode 2 name. In 1.18.33 the plural key no longer fails validation: it is dropped with a compatibility warning in the log, and the plugins it lists never load. Rename it to plugin and keep the same array.
Can an OpenCode plugin block a tool call?#
Yes. Throw an error from tool.execute.before and the tool does not run. The hook receives the tool name in input.tool and the arguments in output.args, so it can also rewrite individual arguments instead of blocking. Blocking from permission.ask does not work in this release because OpenCode never calls that hook.
What is the difference between OpenCode plugins, skills and MCP servers?#
A plugin is code that runs inside OpenCode and can change its behaviour: intercept tool calls, edit prompts, add tools through the tool hook, or register provider auth. A skill is instructions the model loads on demand. An MCP server is a separate process exposing tools over the Model Context Protocol, configured under mcp, with each tool governed by its own permission key.
Sources
- OpenCode official documentation
- OpenCode release notes
packages/plugin/src/index.ts: Plugin, PluginInput and Hooks type definitions (at v1.18.33)packages/opencode/src/config/plugin.ts: plugin discovery, path resolution and dedupe (at v1.18.33)packages/opencode/src/plugin/index.ts: plugin runtime: internal plugins, load order, trigger() (at v1.18.33)packages/opencode/src/effect/runtime-flags.ts: OPENCODE_PURE / OPENCODE_DISABLE_DEFAULT_PLUGINS flags (at v1.18.33)packages/opencode/src/plugin/loader.ts: plugin resolve/install/import pipeline (at v1.18.33)packages/opencode/src/plugin/shared.ts: plugin module shapes and engines.opencode check (at v1.18.33)packages/opencode/src/config/config.ts: config merge order and plugin origins (at v1.18.33)packages/opencode/src/config/paths.ts: config directory discovery (at v1.18.33)packages/opencode/src/agent/agent.ts(at v1.18.33)packages/opencode/src/plugin/pty-environment.ts(at v1.18.33)packages/opencode/src/provider/provider.ts(at v1.18.33)packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts(at v1.18.33)packages/opencode/src/session/compaction.ts(at v1.18.33)packages/opencode/src/session/llm/request.ts(at v1.18.33)packages/opencode/src/session/processor.ts(at v1.18.33)packages/opencode/src/session/prompt.ts(at v1.18.33)packages/opencode/src/session/tools.ts(at v1.18.33)packages/opencode/src/tool/code-mode.ts(at v1.18.33)packages/opencode/src/tool/registry.ts(at v1.18.33)packages/opencode/src/tool/shell.ts(at v1.18.33)packages/opencode/src/provider/auth.ts(at v1.18.33)packages/opencode/src/config/v2-compat.ts(at v1.18.33)packages/sdk/openapi.json: OpenAPI document: Event union (at v1.18.33)
Release-by-release changes: OpenCode version tracker. All OpenCode pages: OpenCode reference index.