Plugins
V2 plugins extend OpenCode in-process. They can transform agents, models, commands, tools, and integrations; intercept model requests and tool execution; and call a subset of the V2 client.
Warning: The plugin API is stable in 2.0.22 but still evolves between minors. V1 plugins do not work in V2 - the API is
define()from@opencode/plugin(Promise) or the Effect entrypoint, with 19 context domains.
Loading Plugins
Add entries to the plugins field in opencode.json(c):
{
"plugins": [
"opencode-acme-plugin@1.2.0",
"./plugins/local.ts",
{
"package": "./plugins/reviewer.ts",
"options": { "agent": "reviewer", "strict": true }
}
]
}
OpenCode also auto-discovers .ts and .js files in .opencode/plugins/ and ~/.config/opencode/plugins/.
Disable plugins by ID with - prefix:
{
"plugins": [
"./plugins/reviewer.ts",
"-acme.reviewer",
"-opencode.provider.*",
"opencode.provider.openai"
]
}
Creating a Plugin
Export Plugin.define() as the module default:
import { Plugin } from "@opencode/plugin"
export default Plugin.define({
id: "acme.reviewer",
setup: async (ctx) => {
await ctx.agent.transform((agents) => {
agents.update("reviewer", (agent) => {
agent.description = "Reviews code for regressions"
agent.mode = "subagent"
})
})
},
})
The setup function runs each time the plugin activates. It may return a cleanup function that OpenCode awaits when the plugin is disabled, reloaded, or shut down.
Plugin Context
The context (ctx) is essentially an OpenCode server client with plugin-only methods:
| Capability | Operations |
|---|---|
ctx.agent | list, get, transform, reload |
ctx.catalog | list, get, transform, reload (providers and models) |
ctx.command | list, transform, reload |
ctx.integration | list, get, connect, transform, reload |
ctx.session | create, get, prompt, hook, interrupt, wait |
ctx.skill | list, transform, reload |
ctx.tool | transform, hook |
ctx.aisdk | hook |
ctx.event | subscribe to server event stream |
ctx.options | Readonly options from config |
ctx.plugin | List active plugin IDs |
ctx.reference | list, transform, reload |
Transform Hooks
Transform hooks modify how OpenCode is configured. They run during setup and on reload:
await ctx.catalog.transform((catalog) => {
catalog.model.update("anthropic", "claude-sonnet-4-5", (draft) => {
draft.name = "Claude Sonnet 4.5"
})
})
ctx.catalog.reload() replays every catalog transform to derive the new catalog. Plugins compose - a later plugin can modify models added by an earlier one.
Runtime Hooks
Runtime hooks intercept live operations:
| Hook | When it fires | Mutable fields |
|---|---|---|
ctx.aisdk.hook("sdk", cb) | Before AI SDK creation | sdk |
ctx.aisdk.hook("language", cb) | Before language model creation | language |
ctx.session.hook("context", cb) | Before model dispatch | system, messages, tools |
ctx.session.hook("http.request", cb) | Before provider HTTP dispatch | request |
ctx.session.hook("http.response", cb) | After provider responds | response |
ctx.tool.hook("execute.before", cb) | Before tool executes | input |
ctx.tool.hook("execute.after", cb) | After tool completes | result or error |
await ctx.session.hook("context", (event) => {
delete event.tools.write
})
await ctx.tool.hook("execute.before", (event) => {
if (event.tool !== "lookup") return
event.input = { ...event.input, source: "plugin" }
})
Gotcha: HTTP hooks apply to native models only. AI SDK models don’t pass through these hooks. Request and response bodies are one-shot streams - use
clone()only when you need a separate reader.
Effect API
For Effect-based applications, use @opencode/plugin/effect:
import { Plugin } from "@opencode/plugin/effect"
import { Effect } from "effect"
export default Plugin.define({
id: "acme.reviewer-effect",
effect: (ctx) =>
Effect.gen(function* () {
yield* ctx.agent.transform((agents) => {
agents.update("reviewer", (agent) => {
agent.description = "Reviews code for regressions"
agent.mode = "subagent"
})
})
}),
})
Context operations return Effects. The plugin effect is scoped - finalizers, fibers, and registrations release when the plugin reloads or unloads.
Adding Custom Tools
Register tools with JSON Schema input and async executors:
await ctx.tool.transform((tools) => {
tools.add("greeting", {
description: "Create a greeting",
input: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
additionalProperties: false,
},
execute: async ({ name }) => {
const text = `Hello, ${name}!`
return { output: { greeting: text }, content: text }
},
})
})
The executor receives a context argument with id, sessionID, agent, messageID, and progress.