OpenCode2 plugin development
OpenCode2 plugin development
Section titled “OpenCode2 plugin development”Related: opencode2-plugin-development · OpenCode2 server API · V2 plugins overview · Configure plugins · Migrate V1→V2
What a plugin is
Section titled “What a plugin is”An OpenCode V2 plugin is a JS/TS module that extends the server (tools, hooks, agents, MCP, worktrees, etc.). The plugin context is essentially an OpenCode server client plus plugin-only transforms, hooks, storage, reloads, and options.
| Kind | Entrypoint | Package |
|---|---|---|
| Promise (default) | Plugin.define({ id, setup(ctx) }) | @opencode/plugin |
| Effect | Plugin.define({ id, effect(ctx) }) | @opencode/plugin/effect (+ effect) |
| CLI / TUI-only | separate CLI plugin config | see “CLI plugins” in docs |
setup runs on load and may return a cleanup function. Registrations dispose when the plugin unloads (or when you call registration.dispose()).
Not covered here: attaching to a running OpenCode2 HTTP server from bots — use opencode2-server-api.
Entry point / “manifest”
Section titled “Entry point / “manifest””Minimal Promise plugin:
import { Plugin } from "@opencode/plugin"
export default Plugin.define({ id: "example", async setup(ctx) { console.log(`loaded in OpenCode ${ctx.app.version}`) console.log(ctx.location.directory) await ctx.storage.set("loaded", true) return () => console.log("unloaded") },})Published package shape:
{ "name": "opencode-acme-plugin", "version": "1.0.0", "type": "module", "exports": { ".": "./src/index.ts" }, "dependencies": { "@opencode/plugin": "<version compatible with target OpenCode>" }}Optional "./rpc" export for a shared RPC contract without loading the implementation.
Stable id is required — scopes ctx.storage, appears in plugin list/diagnostics, and is used with enable/disable wildcards.
How to register tools, commands, hooks
Section titled “How to register tools, commands, hooks”V2 does not return a hooks object from the plugin function (that was V1). Everything registers through ctx.<domain> in setup.
Tools (ctx.tool.transform)
Section titled “Tools (ctx.tool.transform)”JSON Schema input; execute returns structured { content: ... }. Pass context.signal to cancellable work. Namespaces: editor.namespace({ name, description }) then options: { namespace: "acme" } → effective id acme_greeting (dots// → _).
await ctx.tool.transform((editor) => { editor.add({ name: "greeting", description: "Create a greeting", input: { type: "object", properties: { name: { type: "string" } }, required: ["name"], additionalProperties: false, }, async execute(input, context) { await context.progress({ status: "greeting" }) return { content: `Hello ${(input as { name: string }).name}!` } }, })})
await ctx.tool.hook("execute.before", (event) => { if (event.tool === "read") console.log(event.input)})await ctx.tool.hook("execute.after", (event) => { if (event.status === "error") console.error(event.error.message)})Transforms must stay synchronous, cheap, replayable. Load external data before the callback; call ctx.tool.reload() when captured data changes. Later transforms override same effective name.
Commands (ctx.command.transform)
Section titled “Commands (ctx.command.transform)”await ctx.command.transform((editor) => { editor.add({ name: "security-review", description: "Review changes for security issues", execute: async ({ sessionID, prompt, delivery }) => { await ctx.session.prompt({ ...prompt, sessionID, text: `Review these changes for security issues.\n\n${prompt.text}`, delivery, }) }, })})Session hooks (selected)
Section titled “Session hooks (selected)”| Hook | When |
|---|---|
prompt | Before durable prompt admission (rewrites text/files/skills/delivery) |
context | Before agent-loop model call (system, messages, tools, options) |
compaction / generate / title | Auxiliary requests; may set result to skip the model |
model.request | Headers / request settings; optional { providerID } |
http.request / http.response | Native HTTP; bodies are one-shot streams |
retry | Override retry decision/delay |
experimental.ws.* | WebSocket handshake/frames (experimental) |
Also: ctx.permission.hook("evaluate", …), ctx.shell.hook("create.before", …), ctx.event.subscribe({ signal }).
Other transforms (same pattern)
Section titled “Other transforms (same pattern)”agent, provider, model, integration, mcp, reference, skill, vcs, websearch, worktree — each has transform + usually reload.
Config / discovery / install
Section titled “Config / discovery / install”Config key: plugins (plural)
Section titled “Config key: plugins (plural)”{ "$schema": "https://opencode.ai/config.json", "plugins": [ "opencode-acme-plugin", "opencode-acme-plugin@1.2.0", "@acme/opencode-plugin", "./plugins/local", "/absolute/path/plugin.ts", "file:///home/me/plugins/local", { "package": "@acme/opencode-plugin", "options": { "strict": true } } ]}Read options via ctx.options. Relative paths resolve from the config file that contains the entry. Arrays from layered configs merge (lowest → highest precedence), they do not replace.
Auto-discovery
Section titled “Auto-discovery”- Project:
.opencode/plugins/*.ts|jsand immediate package dirs - Global:
~/.config/opencode/plugins/(stock) — on this box / opencode2:~/.config/opencode2/plugins/ - A bare
plugins/next to project-rootopencode.json(c)is not auto-loaded — list it inpluginsor move under.opencode/
Enable/disable order: *, -id, -prefix.*, later IDs re-enable. Built-ins opencode.config.policy and opencode.provider.opencode ignore removals.
CLI (opencode2 on this box)
Section titled “CLI (opencode2 on this box)”opencode2 plugin add opencode-acme-plugin@1.2.0opencode2 plugin add git+https://github.com/anomalyco/opencode-plugin-lane.gitopencode2 plugin listopencode2 plugin list --builtinopencode2 plugin checkopencode2 plugin updateopencode2 plugin remove opencode-acme-plugin@1.2.0Accepts npm names/versions and Git HTTPS/SSH/github: specs (incl. #ref and ::path:). Local paths go in config, not plugin add. Unwatched local deps may need opencode2 service restart / opencode2 reload.
Box paths (opencode2 v2.1.2): config ~/.config/opencode2, data ~/.local/share/opencode2, binary ~/.local/bin/opencode2. Isolated from stock opencode.
Official / reference examples
Section titled “Official / reference examples”| Example | What it shows |
|---|---|
| Docs overview snippets | Plugin.define, tools, hooks, transforms, publish |
| anomalyco/opencode-plugin-lane | Real published V2 plugin: id: "lane", ctx.worktree.transform, Git install via opencode2 plugin add git+https://… |
anomalyco/rift plugins/opencode | id: "rift.workspaces", worktree strategy + tools registration |
Older @opencode-ai/plugin / tool() helpers | V1 — still in many GitHub copies and legacy docs; does not run on V2 without porting |
Lane’s entrypoint (canonical V2 pattern):
import { Plugin } from "@opencode/plugin"import { makeStrategy } from "./strategy"
export default Plugin.define({ id: "lane", async setup(ctx) { const strategy = makeStrategy(ctx.options) await ctx.worktree.transform((editor) => editor.add(strategy)) },})Gotchas
Section titled “Gotchas”- V1 ≠ V2. Returning
{ "tool.execute.before": …, tool: {…} }from a plugin function does not run on V2. Port with migrate-v1:plugin→plugins,@opencode-ai/plugin→@opencode/plugin, hooks →ctx.*.hook, tools →ctx.tool.transform. - Dual support is possible (
setup+ V1server()on one export) but APIs stay separate — no auto-translation. - Transforms replay — no one-shot side effects inside the editor callback; use outer scope +
reload(). - Tool snapshots are frozen per model request; later reload/dispose only affects future requests.
- Prompt hook ≠ context hook — admission vs pre-model; register each kind you need.
- WebSocket providers skip HTTP hooks; use
experimental.ws.*(unstable). - Configured
denypermissions skippermission.evaluatehooks. - opencode2 vs opencode — different XDG dirs; use
opencode2CLI and~/.config/opencode2on this box. - Pin
@opencode/pluginto a version compatible with the OpenCode/opencode2 release; test the installed package, not only a workspace link. - Never put secrets in digests/skills; for server attach/auth see opencode2-server-api.
Sources
Section titled “Sources”- https://opencode.ai/v2/docs/build/plugins
- https://opencode.ai/v2/docs/plugins/
- https://opencode.ai/v2/docs/build/plugins/migrate-v1
- https://opencode.ai/v2/docs/build/plugins/effect/
- https://github.com/anomalyco/opencode-plugin-lane
- Local:
opencode2 --version→opencode v2.1.2;opencode2 plugin --help; npm packageopencode2README (config paths)