# OpenCode Plugin Development Reference ## Overview OpenCode plugins are JavaScript/TypeScript modules that extend OpenCode's functionality by hooking into various events and customizing behavior. Plugins can add custom tools, modify LLM parameters, handle authentication, intercept tool execution, and respond to system events. ## Plugin Locations Plugins are automatically loaded from: 1. **Project-local**: `.opencode/plugin/` directory in your project 2. **Global**: `~/.config/opencode/plugin/` directory **Note**: Local plugin files are auto-discovered and do NOT need to be listed in `opencode.jsonc`'s `plugin` array. The `plugin` array is only for npm package plugins. ## Basic Plugin Structure ### File Format Plugins are `.js` or `.ts` files that export one or more plugin functions: ```javascript export const MyPlugin = async ({ project, client, $, directory, worktree }) => { // Initialization code here console.log("Plugin initialized"); return { // Hook implementations }; }; ``` ### Plugin Context (Input Parameters) Every plugin function receives a context object with: | Parameter | Type | Description | |-----------|------|-------------| | `project` | `Project` | Current project information | | `directory` | `string` | Current working directory | | `worktree` | `string` | Git worktree path | | `client` | `OpencodeClient` | OpenCode SDK client for API access | | `$` | `BunShell` | Bun's shell API for executing commands | ### Plugin Return Value (Hooks) Plugins return an object containing hook implementations. All hooks are optional. ## Available Hooks ### 1. `event` Hook Respond to OpenCode system events. ```javascript event: async ({ event }) => { if (event.type === "session.idle") { // OpenCode is waiting for user input } } ``` **Common Event Types:** - `session.idle` - Session is waiting for user input - `session.start` - Session has started - `session.end` - Session has ended - Additional events available via SDK ### 2. `config` Hook React to configuration changes. ```javascript config: async (config) => { console.log("Config:", config.model, config.theme); } ``` ### 3. `tool` Hook Add custom tools that the LLM can call. ```javascript import { tool } from "@opencode-ai/plugin"; return { tool: { mytool: tool({ description: "Description shown to LLM", args: { query: tool.schema.string().describe("Query parameter"), count: tool.schema.number().optional().describe("Optional count") }, async execute(args, context) { // context contains: { agent, sessionID, messageID } return `Result: ${args.query}`; } }) } }; ``` **Tool Schema Types** (using Zod): - `tool.schema.string()` - `tool.schema.number()` - `tool.schema.boolean()` - `tool.schema.object({ ... })` - `tool.schema.array(...)` - `.optional()` - Make parameter optional - `.describe("...")` - Add description for LLM ### 4. `auth` Hook Provide custom authentication methods. ```javascript auth: { provider: "my-service", methods: [ { type: "api", label: "API Key" }, { type: "oauth", label: "OAuth Login", authorize: async () => ({ url: "https://...", instructions: "Login instructions", method: "auto", callback: async () => ({ type: "success", key: "token" }) }) } ] } ``` ### 5. `chat.message` Hook Called when a new user message is received. ```javascript "chat.message": async ({}, output) => { console.log("Message:", output.message.text); console.log("Parts:", output.parts); } ``` ### 6. `chat.params` Hook Modify parameters sent to the LLM. ```javascript "chat.params": async (input, output) => { // input: { model, provider, message } output.temperature = 0.7; output.topP = 0.95; output.options = { /* custom options */ }; } ``` ### 7. `permission.ask` Hook Intercept permission requests. ```javascript "permission.ask": async (permission, output) => { if (permission.tool === "bash" && permission.args.command.includes("rm")) { output.status = "deny"; // or "allow" or "ask" } } ``` ### 8. `tool.execute.before` Hook Intercept tool execution before it runs. ```javascript "tool.execute.before": async (input, output) => { // input: { tool, sessionID, callID } // output: { args } if (input.tool === "read" && output.args.filePath.includes(".env")) { throw new Error("Cannot read .env files"); } } ``` ### 9. `tool.execute.after` Hook Modify tool execution results. ```javascript "tool.execute.after": async (input, output) => { // input: { tool, sessionID, callID } // output: { title, output, metadata } console.log(`Tool ${input.tool} returned:`, output.output); } ``` ## Using the OpenCode SDK Client The `client` parameter provides full API access: ### Common Operations ```javascript // Get current project const project = await client.project.current(); // List sessions const sessions = await client.session.list(); // Read a file const content = await client.file.read({ query: { path: "src/index.ts" } }); // Search for text const matches = await client.find.text({ query: { pattern: "TODO" } }); // Show toast notification await client.tui.showToast({ body: { message: "Task complete", variant: "success" } }); // Send a prompt await client.session.prompt({ path: { id: sessionID }, body: { model: { providerID: "anthropic", modelID: "claude-3-5-sonnet-20241022" }, parts: [{ type: "text", text: "Hello!" }] } }); ``` ### Available SDK Methods **App**: `app.log()`, `app.agents()` **Project**: `project.list()`, `project.current()` **Sessions**: `session.list()`, `session.get()`, `session.create()`, `session.delete()`, etc. **Files**: `file.read()`, `file.status()`, `find.text()`, `find.files()`, `find.symbols()` **TUI**: `tui.appendPrompt()`, `tui.showToast()`, `tui.openHelp()`, etc. **Config**: `config.get()`, `config.providers()` **Events**: `event.subscribe()` (for event streaming) See full SDK documentation: https://opencode.ai/docs/sdk/ ## Using Bun Shell (`$`) Execute shell commands easily: ```javascript // Simple command await $`notify-send "Hello"`; // Get output const text = await $`git status`.text(); const json = await $`hyprctl clients -j`.json(); // Command with variables const file = "test.txt"; await $`cat ${file}`; // Array of arguments const args = ["notify-send", "-u", "normal", "Title", "Body"]; await $`${args}`; ``` ## Complete Example: Notification Plugin ```javascript export const NotificationPlugin = async ({ project, client, $, directory, worktree }) => { console.log("Notification plugin initialized"); return { // Send notification when OpenCode is idle event: async ({ event }) => { if (event.type === "session.idle") { const pid = process.pid; const iconPath = `${process.env.HOME}/.config/opencode/icon.png`; try { // Get window info from Hyprland const clientsJson = await $`hyprctl clients -j`.text(); const clients = JSON.parse(clientsJson); const window = clients.find(c => c.pid === pid); // Send notification with action const result = await $`notify-send -a "OpenCode" -u normal -i ${iconPath} -A focus=Focus "OpenCode Ready" "Waiting for input in ${directory}"`.text(); // Handle action click if (result.trim() === "focus" && window?.address) { await $`hyprctl dispatch focuswindow address:${window.address}`; } } catch (error) { console.error("Notification error:", error); } } }, // Add custom tool tool: { notify: tool({ description: "Send a system notification", args: { message: tool.schema.string().describe("Notification message"), urgency: tool.schema.enum(["low", "normal", "critical"]).optional() }, async execute(args) { await $`notify-send -u ${args.urgency || "normal"} "OpenCode" ${args.message}`; return "Notification sent"; } }) }, // Modify LLM parameters "chat.params": async (input, output) => { // Lower temperature for code-focused tasks if (input.message.text?.includes("refactor") || input.message.text?.includes("bug")) { output.temperature = 0.3; } }, // Prevent dangerous operations "tool.execute.before": async (input, output) => { if (input.tool === "bash" && output.args.command.includes("rm -rf")) { throw new Error("Dangerous command blocked by plugin"); } } }; }; ``` ## TypeScript Support For type-safe plugins, import types from the plugin package: ```typescript import type { Plugin } from "@opencode-ai/plugin"; export const MyPlugin: Plugin = async (ctx) => { return { // Type-safe hook implementations }; }; ``` ## Best Practices 1. **Error Handling**: Always wrap risky operations in try-catch blocks 2. **Async Operations**: All hook functions should be async 3. **Console Logging**: Use `console.log()` for debugging - visible in OpenCode logs 4. **Resource Cleanup**: Clean up resources when plugins are reloaded 5. **Minimal Processing**: Keep hooks fast to avoid blocking OpenCode 6. **Security**: Validate inputs, especially in custom tools 7. **Documentation**: Add clear descriptions to custom tools for the LLM ## Common Use Cases ### System Integration - Send desktop notifications (Linux, macOS, Windows) - Integrate with window managers (Hyprland, i3, etc.) - System clipboard operations - File system watching ### Development Workflow - Run tests on code changes - Format code automatically - Update documentation - Git operations ### LLM Enhancement - Add domain-specific tools - Custom prompt preprocessing - Response filtering - Context augmentation ### Security & Compliance - Block dangerous commands - Prevent access to sensitive files - Audit tool usage - Rate limiting ## Debugging 1. **View Logs**: Run OpenCode with debug output ```bash G_MESSAGES_DEBUG=all opencode ``` 2. **Console Logging**: Use `console.log()`, `console.error()` in plugins 3. **Test Independently**: Test shell commands and SDK calls outside plugins first 4. **Hot Reload**: Plugins are reloaded when files change (in development mode) ## Related Documentation - [OpenCode Plugins](https://opencode.ai/docs/plugins/) - [OpenCode SDK](https://opencode.ai/docs/sdk/) - [Custom Tools](https://opencode.ai/docs/custom-tools/) - [Bun Shell API](https://bun.sh/docs/runtime/shell) - [Zod Documentation](https://zod.dev/) ## Template Plugin ```javascript /** * Template Plugin * Description: What this plugin does */ export const TemplatePlugin = async ({ project, client, $, directory, worktree }) => { // Initialization console.log("Template plugin initialized"); // You can store state here let pluginState = {}; return { // Implement the hooks you need event: async ({ event }) => { // Handle events }, tool: { // Add custom tools }, "chat.params": async (input, output) => { // Modify LLM parameters }, // ... other hooks as needed }; }; ```