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 V2 plugin API is beta. Entrypoints, hooks, and configuration may change before stable release. V1 plugins will not work in V2.
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-ai/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-ai/plugin/effect:
import { Plugin } from "@opencode-ai/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.