03. Creating plugins¶
🎯 Objective¶
Write plugins that add tools, hooks and transforms to OpenCode, and expose methods/events via RPC.
📋 Basic structure¶
import { Plugin } from "@opencode/plugin"
export default Plugin.define({
id: "example",
async setup(ctx) {
await ctx.storage.set("loaded", true)
},
})
Plugins in .opencode/plugins/ load automatically. setup runs on load and can return a cleanup function. The ctx is essentially an OpenCode server client plus plugin-only methods.
➕ Adding a tool¶
import { Plugin } from "@opencode/plugin"
export default Plugin.define({
id: "acme",
async setup(ctx) {
const registration = await ctx.tool.transform((editor) => {
editor.namespace({ name: "acme", description: "Customer account tools" })
editor.add({
name: "greeting",
description: "Creates a greeting",
input: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
additionalProperties: false,
},
options: { namespace: "acme", codemode: true },
execute: async (input, context) => {
await context.progress({ status: "greeting" })
return { content: `Hello ${(input as { name: string }).name}!` }
},
})
})
return () => registration.dispose()
},
})
- Pass
context.signalto cancellable work (e.g.fetch) - Disposing the registration removes the tool and reapplies the others
🔧 Transforms (agents, providers, models, commands, MCP…)¶
Each domain has transform + reload. Examples:
// agents
await ctx.agent.transform((editor) => {
editor.update("build", (agent) => {
agent.description = "Implements features and fixes bugs"
})
})
// models: remove expensive models
await ctx.model.transform((editor) => {
editor.list().filter((m) => m.cost.some((t) => t.output > 20))
.forEach((m) => editor.remove(m.providerID, m.id))
})
// MCP
await ctx.mcp.transform((editor) => {
editor.set("docs", { type: "remote", url: "https://mcp.example.com" })
})
Available domains: agent, provider, model, command, integration, mcp, reference, skill, tool, vcs, worktree, websearch, credential.
🪝 Hooks¶
They intercept live operations. Examples:
// redact secrets in incoming prompts
await ctx.session.hook("prompt", (event) => {
event.prompt.text = event.prompt.text.replaceAll("company-secret", "[redacted]")
})
// adjust the model request
await ctx.session.hook("context", (event) => {
event.system.push({ type: "text", text: "Focus on correctness." })
event.options.temperature = 0.2
})
Session hooks include prompt, context, compaction, generate, title, model.request, http.request, http.response.
🔌 RPC: methods and events between plugins/clients¶
import { Rpc } from "@opencode/plugin/rpc"
export const Acme = Rpc.define({
id: "acme",
methods: {
search: {
input: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
output: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
},
},
events: {
updated: { schema: { type: "object", properties: { itemID: { type: "string" } } } },
},
})
Register inside setup and call it from another plugin or over HTTP:
const registration = await ctx.rpc.register(Acme, {
search: async (input, context) => ({ text: await findText((input as any).query, { signal: context.signal }) }),
})
await registration.events.emit("updated", { itemID: "item-1" })
HTTP client: client.rpc(Acme).search({ query: "hello" }) or POST /api/rpc/acme/search with { "input": ... }.
📦 Publishing¶
Package it as an npm package (@acme/opencode-plugin) whose entrypoint exports Plugin.define(...). Enable it with opencode plugin add.
💡 Examples¶
exemplos/plugins/greeting/index.ts— plugin with a toolexemplos/plugins/acme-rpc.ts— plugin with RPC (methods + events)
❓ Hands-on challenge¶
- Create a plugin with a
git_branch_nametool that returns the current branch - Add a
prompthook that appends "answer in English" - Expose an RPC method and call it from a client
- Publish it locally and load it via
pluginsinopencode.json
📚 Official source¶
Next step: Lesson 04 — Web search · Back to module