# OpenCode V2 Plugin Documentation > Сгенерировано: 2026-09-25 | Версия OpenCode: v2.x | @opencode/plugin: v2.x --- ## Table of Contents 1. [Overview](#overview) 2. [Plugin Loading](#plugin-loading) 3. [Plugin Structure](#plugin-structure) 4. [Plugin API (Promise API)](#plugin-api-promise-api) 5. [Context Object](#context-object) 6. [Hooks](#hooks) 7. [Custom Tools](#custom-tools) 8. [Events](#events) 9. [Configuration](#configuration) 10. [External Dependencies](#external-dependencies) 11. [Local Plugin Pitfalls](#local-plugin-pitfalls) 12. [Debugging](#debugging) 13. [Complete Examples](#complete-examples) --- ## Overview OpenCode V2 plugins extend the agent by adding custom tools, hooks, event handlers, and integrations. A plugin is a TypeScript/JavaScript module that uses `Plugin.define()` from `@opencode/plugin` to register capabilities. ### Key Concepts - **Plugin**: A module that calls `Plugin.define({ id, setup(ctx) })` and exports it as default. - **setup(ctx)**: An async function that receives a context object and returns a cleanup function. - **ctx**: The plugin context providing access to all OpenCode domains (session, tool, event, storage, etc.). - **Hooks**: Functions registered on domain-specific APIs (e.g., `ctx.session.hook()`, `ctx.tool.hook()`). - **Events**: Subscribable event streams via `ctx.event.subscribe()`. - **Custom Tools**: Tools defined with `tool()` helper and registered via `ctx.tool.transform()`. --- ## Plugin Loading ### Discovery Locations OpenCode discovers plugins from multiple sources, loaded in this order: | Source | Path | Auto-discovered | |--------|------|-----------------| | Global config | `~/.config/opencode/opencode.json` | N/A (explicit) | | Project config | `opencode.json` | N/A (explicit) | | Global plugin dir | `~/.config/opencode/plugins/` | Yes | | Project plugin dir | `.opencode/plugins/` | Yes | ### Discovery Rules **Global plugin directory** (`~/.config/opencode/plugins/`): - Direct `.ts` and `.js` files are loaded as plugins - Immediate subdirectories containing a `package.json` are loaded as plugin packages - Nested directories are NOT auto-discovered **Project plugin directory** (`.opencode/plugins/`): - Same rules as global ### Explicit Configuration Plugins can also be explicitly listed in `opencode.json`: ```jsonc { "plugins": [ // npm package "opencode-acme-plugin", // npm package with version "opencode-acme-plugin@1.2.0", // scoped npm package "@acme/opencode-plugin", // local file path (relative to config file) "./plugins/local", // absolute file path "/home/user/plugin.ts", // file:// URL "file:///home/user/plugins/local", // package with options { "package": "@acme/opencode-plugin", "options": { "agent": "reviewer", "strict": true } } ] } ``` ### Plugin Control Prefix an ID or wildcard with `-` to disable: ```jsonc { "plugins": [ "*", // enable all "-opencode.provider.*", // disable all providers "opencode.provider.openai", // re-enable OpenAI "-acme.reviewer" // disable specific plugin ] } ``` Two built-in plugins always ignore removals: `opencode.config.policy` and `opencode.provider.opencode`. --- ## Plugin Structure ### Minimal Plugin ```typescript // ~/.config/opencode/plugins/my-plugin.ts export default { id: "my-plugin", async setup(ctx) { console.log("Plugin loaded!") // cleanup function (optional) return () => { console.log("Plugin cleaning up") } }, } ``` ### Plugin with Package.json (Directory Plugin) ``` ~/.config/opencode/plugins/my-plugin/ ├── package.json # Required: defines name, dependencies ├── index.ts # Entry point (or main field in package.json) ├── helper.ts # Additional modules └── node_modules/ # Dependencies (manually installed) ``` **package.json**: ```json { "name": "my-opencode-plugin", "version": "1.0.0", "main": "index.ts", "dependencies": { "@opencode/plugin": "^2.0.16", "some-npm-package": "^1.0.0" } } ``` **index.ts**: ```typescript import { Plugin, tool } from "@opencode/plugin" export default Plugin.define({ id: "my-opencode-plugin", async setup(ctx) { // Plugin initialization const result = await someNpmPackage.doSomething() console.log(`Result: ${result}`) return { // hooks } }, }) ``` --- ## Plugin API (Promise API) The Promise API is the primary way to write V2 plugins: ```typescript import { Plugin } from "@opencode/plugin" export default Plugin.define({ id: "my-plugin", async setup(ctx) { // Register hooks, tools, etc. // ... // Return cleanup function (optional) return () => { // Release resources } }, }) ``` ### Plugin.define() Signature ```typescript Plugin.define({ id: string, // Unique plugin identifier (required) async setup(ctx): PluginContext // Setup function (required) }): PluginDefinition ``` ### Effect API (Alternative) For plugins that need Effect-TS: ```typescript import { Plugin } from "@opencode/plugin/effect" ``` --- ## Context Object The `ctx` parameter in `setup()` provides access to all OpenCode domains: ### Core Properties | Property | Type | Description | |----------|------|-------------| | `ctx.location` | Location | Where this plugin was loaded (NOT where observed sessions are) | | `ctx.location.directory` | string | Working directory of the plugin's location | | `ctx.location.project` | ProjectInfo | Project info at plugin load time | ### Domain APIs | Domain | Purpose | |--------|---------| | `ctx.app` | Application-level operations (logging, options) | | `ctx.session` | Session management, prompts, hooks | | `ctx.tool` | Tool hooks, transforms, custom tools | | `ctx.event` | Event subscription | | `ctx.storage` | Persistent key-value storage | | `ctx.shell` | Shell command execution | | `ctx.command` | Command transforms | | `ctx.permission` | Permission hooks | | `ctx.provider` | Provider transforms | | `ctx.model` | Model transforms | | `ctx.integration` | Integration/connection APIs | | `ctx.mcp` | MCP server management | | `ctx.skill` | Skill management | | `ctx.agent` | Agent configuration | | `ctx.vcs` | Version control operations | | `ctx.websearch` | Web search | | `ctx.rpc` | RPC calls | | `ctx.reference` | Reference management | | `ctx.worktree` | Git worktree operations | ### Important: ctx.location vs Observed Events > **CRITICAL**: `ctx.location` describes where the plugin was loaded, NOT the location of observed sessions or events. Never infer a session's location from `ctx.location`. Use event/session data instead. --- ## Hooks ### Session Hooks ```typescript // Prompt hook - called before a prompt is sent ctx.session.hook("prompt", (event) => { // Modify event before sending event.metadata = { ...event.metadata, plugin: "my-plugin" } }) // Context hook - modify context sent to LLM ctx.session.hook("context", (input, output) => { output.system.push(`Rules go here`) }) // Compaction hook - customize context preservation ctx.session.hook("compaction", (input, output) => { output.context.push(`## Preserved state\n- Current task: ...`) }) ``` ### Tool Hooks ```typescript // Before tool execution ctx.tool.hook("execute.before", (input, output) => { if (input.tool === "bash" && output.args.command.includes("rm -rf")) { throw new Error("Dangerous command blocked") } }) // After tool execution ctx.tool.hook("execute.after", (input) => { console.log(`Tool ${input.tool} completed`) }) ``` ### Shell Hooks ```typescript // Modify shell creation ctx.shell.hook("create.before", (input, output) => { output.env.MY_API_KEY = "secret" output.env.PROJECT_ROOT = input.cwd }) ``` ### Permission Hooks ```typescript ctx.permission.hook("evaluate", (permission, output) => { if (permission.type === "read_file") { output.status = "allow" } }) ``` ### Event Subscription ```typescript const controller = new AbortController() void (async () => { for await (const event of ctx.event.subscribe({ signal: controller.signal })) { if (event.type === "session.created") { console.log(`New session: ${event.sessionID}`) } if (event.type === "session.idle") { console.log("Session idle") } if (event.type === "message.updated") { console.log("Message updated") } } })() // Cleanup return () => { controller.abort() } ``` ### Available Events | Event Type | Description | |------------|-------------| | `session.created` | New session created | | `session.updated` | Session updated | | `session.deleted` | Session deleted | | `session.error` | Session error | | `session.idle` | Session became idle | | `session.compacted` | Session compacted | | `message.updated` | Message updated | | `message.removed` | Message removed | | `file.edited` | File edited | | `permission.asked` | Permission requested | | `permission.replied` | Permission responded | | `server.connected` | Server connected | | `tool.execute.before` | Tool about to execute | | `tool.execute.after` | Tool finished executing | --- ## Custom Tools ```typescript import { tool } from "@opencode/plugin" // In setup(): ctx.tool.transform((editor) => { editor.add({ name: "my-tool", description: "Does something useful", async execute({ sessionID }, delivery) { // Tool implementation return "Result" }, }) }) ``` ### Using the `tool()` Helper ```typescript import { tool } from "@opencode/plugin" const myTool = tool({ description: "Search the web", args: { query: tool.schema.string().describe("Search query"), maxResults: tool.schema.number().optional().describe("Max results"), }, async execute(args, context) { const { sessionID, agent } = context // Implementation using ctx return `Results for: ${args.query}` }, }) ``` --- ## Events ### Subscribing to Events ```typescript const eventController = new AbortController() void (async () => { for await (const event of ctx.event.subscribe({ signal: eventController.signal })) { // Handle event console.log(`Event: ${event.type}`) } })() ``` ### Event Update Types When handling `session.updated` events, the `event.update` field can be: | Type | Description | |------|-------------| | `agent_message_chunk` | Streaming text from the agent | | `tool_call` | Tool invocation | | `tool_call_update` | Tool progress/completion | | `user_message_chunk` | User message fragment | | `agent_thought_chunk` | Agent reasoning/thought | --- ## Configuration ### Plugin Options Configure via `opencode.json`: ```jsonc { "plugins": [ { "package": "./plugins/my-plugin", "options": { "apiKey": "secret", "enabled": true } } ] } ``` Access in plugin: ```typescript export default Plugin.define({ id: "my-plugin", async setup(ctx) { const apiKey = ctx.options.apiKey const enabled = ctx.options.enabled }, }) ``` ### Structured Logging ```typescript await ctx.app.log({ body: { service: "my-plugin", level: "info", // debug, info, warn, error message: "Plugin initialized", extra: { key: "value" }, }, }) ``` --- ## External Dependencies ### How Dependencies Work OpenCode V2 does NOT automatically install dependencies for local plugins. You must manually install them. ### For Global Plugin Directory (`~/.config/opencode/plugins/`) Dependencies must be installed in the config directory: ```json // ~/.config/opencode/package.json { "dependencies": { "@opencode/plugin": "^2.0.16", "some-npm-package": "^1.0.0" } } ``` OpenCode runs `bun install` at startup for this file. ### For Local Plugin Package Directory If your plugin is in a subdirectory with its own `package.json`: ``` ~/.config/opencode/plugins/my-plugin/ ├── package.json ├── index.ts └── node_modules/ # <-- You must create this! ``` **You must manually run `bun install` (or `npm install`) inside the plugin directory:** ```bash cd ~/.config/opencode/plugins/my-plugin bun install ``` > **CRITICAL**: OpenCode does NOT automatically run `bun install` in plugin subdirectories. Dependencies in a plugin's own `package.json` will NOT be available unless you manually install them. ### For npm Package Plugins Dependencies are installed automatically when the plugin is added via `opencode plugin add`. --- ## Local Plugin Pitfalls ### Pitfall 1: Dependencies Not Auto-Installed **Problem**: Plugin has `package.json` with dependencies, but they're not available at runtime. **Solution**: Run `bun install` in the plugin directory: ```bash cd ~/.config/opencode/plugins/my-plugin bun install ``` ### Pitfall 2: Wrong Import Path **Problem**: Using `@opencode-ai/plugin` (V1) instead of `@opencode/plugin` (V2). **Solution**: Use the V2 import: ```typescript // V2 (correct) import { Plugin } from "@opencode/plugin" // V1 (wrong for V2) import type { Plugin } from "@opencode-ai/plugin" ``` ### Pitfall 3: Mixing CommonJS and ESM **Problem**: Using `require()` and `module.exports` in a TypeScript file loaded as ESM. **Solution**: Use ES module syntax throughout: ```typescript // Wrong - CommonJS in ESM context const { MatrixClient } = require("matrix-bot-sdk") module.exports = { MatrixBotClient } // Correct - ES modules import { MatrixClient } from "matrix-bot-sdk" export class MatrixBotClient { ... } ``` ### Pitfall 4: Plugin Not Loading (Silent Failure) **Problem**: Plugin has a syntax error or import error, but no log is written. **Solution**: 1. Check for TypeScript errors: `tsc --noEmit` in the plugin directory 2. Use `client.app.log()` for structured logging 3. Verify the plugin path is correct in `opencode.json` 4. Check that `@opencode/plugin` is installed in the dependency location ### Pitfall 5: Context Destructuring Error **Problem**: Treating `ctx` as the client directly: ```typescript // Wrong export default Plugin.define({ id: "my-plugin", async setup(client) { await client.session.prompt(...) // FAILS: client.session.prompt doesn't exist }, }) // Correct export default Plugin.define({ id: "my-plugin", async setup(ctx) { await ctx.session.prompt(...) // Correct }, }) ``` ### Pitfall 6: Hook Name Case Sensitivity **Problem**: Using incorrect hook names. **Solution**: Verify exact hook names from the API reference. Hook names are case-sensitive. ### Pitfall 7: Missing Cleanup **Problem**: Event subscriptions or intervals not cleaned up on plugin unload. **Solution**: Return a cleanup function: ```typescript export default Plugin.define({ id: "my-plugin", async setup(ctx) { const interval = setInterval(() => { /* ... */ }, 60000) const controller = new AbortController() // Subscribe to events void (async () => { for await (const event of ctx.event.subscribe({ signal: controller.signal })) { // handle } })() // Cleanup return () => { controller.abort() clearInterval(interval) } }, }) ``` ### Pitfall 8: Using ctx.location for Session Data **Problem**: Inferring session location from `ctx.location`: ```typescript // Wrong const sessionDir = ctx.location.directory // This is where plugin was loaded! // Correct const sessionDir = event.directory // Use event/session data ``` --- ## Debugging ### Checklist 1. **Plugin not loading?** - Check for TypeScript errors: `tsc --noEmit` - Check import paths are correct - Verify dependencies are installed - Check plugin path in `opencode.json` 2. **Hooks not firing?** - Verify hook names match exactly (case-sensitive) - Check hook registration syntax 3. **State not persisting?** - Use session-keyed Maps, not global variables - Use `ctx.storage` for persistent data 4. **`ctx.session.prompt()` failing?** - Verify destructuring: `async setup(ctx)` not `async setup(client)` - Check session ID is valid ### Logging Use structured logging via `ctx.app.log()`: ```typescript await ctx.app.log({ body: { service: "my-plugin", level: "info", message: "Plugin initialized", extra: { key: "value" }, }, }) ``` Levels: `debug`, `info`, `warn`, `error`. ### Verbose Mode Run OpenCode with `--verbose` for more detailed plugin loading output. --- ## Complete Examples ### Example 1: Simple Notification Plugin ```typescript import { Plugin } from "@opencode/plugin" export default Plugin.define({ id: "notification-plugin", async setup(ctx) { void (async () => { for await (const event of ctx.event.subscribe({ signal: new AbortController().signal, })) { if (event.type === "session.idle") { await ctx.app.log({ body: { service: "notification", level: "info", message: "Session completed", }, }) } } })() return () => {} }, }) ``` ### Example 2: Plugin with Matrix Integration ```typescript import { Plugin } from "@opencode/plugin" import { MatrixClient } from "matrix-bot-sdk" import { marked } from "marked" export default Plugin.define({ id: "matrix-bot", async setup(ctx) { const options = ctx.options if (!options.homeserver) { await ctx.app.log({ body: { service: "matrix", level: "warn", message: "Disabled (no homeserver)" }, }) return () => {} } const client = new MatrixClient(options.homeserver) const sessions = new Map() // Login if (options.accessToken) { await client.startWithToken("m.login.token", { user_id: options.userId, access_token: options.accessToken, }) } else { await client.startWithPassword(options.userId, options.password) } const userId = await client.getUserId() await ctx.app.log({ body: { service: "matrix", level: "info", message: `Started as ${userId}` }, }) // Listen for messages client.on("room.message", async (roomId, event) => { if (event.type !== "m.room.message" || event.content.msgtype !== "m.text") return if (event.sender === userId) return const text = event.content.body?.trim() if (!text?.startsWith("!oc ")) return const query = text.slice(4).trim() const sessionId = roomId // Send prompt const result = await ctx.session.prompt({ sessionID: sessionId, text: query, }) // Send response await client.sendText(roomId, result) }) // Event subscription for response streaming const controller = new AbortController() void (async () => { for await (const event of ctx.event.subscribe({ signal: controller.signal })) { if (event.type === "session.updated" && event.update?.type === "agent_message_chunk") { // Stream response to Matrix } } })() return () => { controller.abort() client.stop() } }, }) ``` ### Example 3: Plugin with Custom Tool ```typescript import { Plugin, tool } from "@opencode/plugin" export default Plugin.define({ id: "custom-tools", async setup(ctx) { ctx.tool.transform((editor) => { editor.add({ name: "git-status", description: "Show git status of the current project", async execute(_, delivery) { const result = await ctx.shell.execute("git status --porcelain") return result.stdout }, }) editor.add({ name: "search-files", description: "Search for files matching a pattern", async execute(args: { pattern: string }, delivery) { const result = await ctx.shell.execute(`find . -name "${args.pattern}" -type f`) return result.stdout }, }) }) return () => {} }, }) ``` ### Example 4: Plugin with Persistent Storage ```typescript import { Plugin } from "@opencode/plugin" export default Plugin.define({ id: "state-tracker", async setup(ctx) { // Initialize from storage const version = await ctx.storage.get("state-tracker/version") if (!version) { await ctx.storage.set("state-tracker/version", "1.0.0") await ctx.storage.set("state-tracker/started_at", new Date().toISOString()) } // Track sessions const sessions = new Map() const controller = new AbortController() void (async () => { for await (const event of ctx.event.subscribe({ signal: controller.signal })) { if (event.type === "session.created" && event.sessionID) { sessions.set(event.sessionID, { count: 0 }) } if (event.type === "session.deleted" && event.sessionID) { sessions.delete(event.sessionID) } } })() return () => { controller.abort() } }, }) ``` --- ## Migration from V1 to V2 ### Import Changes | V1 | V2 | |----|----| | `import type { Plugin } from "@opencode-ai/plugin"` | `import { Plugin } from "@opencode/plugin"` | | `export const MyPlugin = async (ctx) => ({ ...hooks })` | `export default Plugin.define({ id, async setup(ctx) { ... } })` | ### Hook Mapping | V1 Hook | V2 API | |---------|--------| | `event: async ({ event }) => {}` | `ctx.event.subscribe()` | | `"tool.execute.before"` | `ctx.tool.hook("execute.before", ...)` | | `"tool.execute.after"` | `ctx.tool.hook("execute.after", ...)` | | `"chat.message"` | `ctx.session.hook("prompt", ...)` | | `"chat.params"` | `ctx.session.hook("context", ...)` | | `permission.ask` | `ctx.permission.hook("evaluate", ...)` | | `dispose` | `return cleanup` from setup | | `config` | transforms on affected domains | ### Context Changes | V1 | V2 | |----|----| | `ctx.client` | Domain APIs on `ctx` | | `ctx.project` | `ctx.location.project` | | `ctx.directory` | `ctx.location.directory` | | `ctx.$` | Explicitly imported shell helpers | --- ## Quick Reference ### File: `~/.config/opencode/opencode.json` ```jsonc { "plugins": [ // npm package "opencode-helicone-session", // local directory "./plugins/local", // with options { "package": "./plugins/matrix-plugin", "options": { "homeserver": "https://matrix.org", "userId": "@bot:matrix.org", "password": "secret" } } ] } ``` ### File: `~/.config/opencode/plugins/my-plugin/package.json` ```json { "name": "my-opencode-plugin", "version": "1.0.0", "main": "index.ts", "dependencies": { "@opencode/plugin": "^2.0.16" } } ``` ### File: `~/.config/opencode/plugins/my-plugin/index.ts` ```typescript import { Plugin } from "@opencode/plugin" export default Plugin.define({ id: "my-opencode-plugin", async setup(ctx) { // Initialization await ctx.app.log({ body: { service: "my-plugin", level: "info", message: "Loaded" } }) // Event subscription const controller = new AbortController() void (async () => { for await (const event of ctx.event.subscribe({ signal: controller.signal })) { if (event.type === "session.idle") { await ctx.app.log({ body: { service: "my-plugin", level: "info", message: "Session idle" } }) } } })() return () => { controller.abort() } }, }) ``` ### Install Dependencies ```bash cd ~/.config/opencode/plugins/my-plugin bun install ``` --- ## References - [Official OpenCode Plugin Docs](https://opencode.ai/v2/docs/plugins) - [Plugin Source Code](packages/plugin/src/index.ts) - [Tool Definition](packages/plugin/src/tool.ts) - [Plugin Loader](packages/opencode/src/plugin/index.ts) - [V1 to V2 Migration Guide](https://gist.github.com/yohi/0c3d8edea98d86e7f5c8d6c2d9f93d2d) - [Plugin Development Reference](https://github.com/growwithsmc/opencode-plugin-dev)