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:

CapabilityOperations
ctx.agentlist, get, transform, reload
ctx.cataloglist, get, transform, reload (providers and models)
ctx.commandlist, transform, reload
ctx.integrationlist, get, connect, transform, reload
ctx.sessioncreate, get, prompt, hook, interrupt, wait
ctx.skilllist, transform, reload
ctx.tooltransform, hook
ctx.aisdkhook
ctx.eventsubscribe to server event stream
ctx.optionsReadonly options from config
ctx.pluginList active plugin IDs
ctx.referencelist, 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:

HookWhen it firesMutable fields
ctx.aisdk.hook("sdk", cb)Before AI SDK creationsdk
ctx.aisdk.hook("language", cb)Before language model creationlanguage
ctx.session.hook("context", cb)Before model dispatchsystem, messages, tools
ctx.session.hook("http.request", cb)Before provider HTTP dispatchrequest
ctx.session.hook("http.response", cb)After provider respondsresponse
ctx.tool.hook("execute.before", cb)Before tool executesinput
ctx.tool.hook("execute.after", cb)After tool completesresult 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.