OpenCode Plugin Development
Plugin Fundamentals
What is a Plugin?
A plugin is a JavaScript/TypeScript module that extends OpenCode by hooking into events, adding custom tools, and modifying behavior. Plugins can integrate with external services, enforce policies, and automate workflows.
Plugin Structure
A plugin exports one or more functions that receive a context object and return a hooks object:
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
return {
// Hook implementations
}
}
Context Object
Plugins receive:
- •
project: Current project information - •
directory: Current working directory - •
worktree: Git worktree path - •
client: OpenCode SDK client for AI interaction - •
$: Bun's shell API for executing commands
Event Hooks
Tool Events
- •
tool.execute.before: Before any tool executes - •
tool.execute.after: After any tool executes
Session Events
- •
session.created: New session created - •
session.updated: Session metadata updated - •
session.deleted: Session deleted - •
session.status: Session status changes - •
session.error: Session errors - •
session.idle: Session becomes idle - •
session.compacted: Session compacted - •
session.diff: Session diff generated
Message Events
- •
message.updated: Message content changed - •
message.removed: Message deleted - •
message.part.updated: Message part changed - •
message.part.removed: Message part deleted
File Events
- •
file.edited: File modified - •
file.watcher.updated: File watcher detected change
Command Events
- •
command.executed: Slash command executed
Server Events
- •
server.connected: Client connected to server
Permission Events
- •
permission.updated: Permissions changed - •
permission.replied: Permission request responded to
TUI Events
- •
tui.prompt.append: Text appended to prompt - •
tui.command.execute: Command executed - •
tui.toast.show: Toast notification shown
Installation Events
- •
installation.updated: Installation status changed
LSP Events
- •
lsp.client.diagnostics: LSP diagnostics available - •
lsp.updated: LSP status updated
Todo Events
- •
todo.updated: Todo list changed
Plugin Installation
Local Installation
Place files in:
- •
.opencode/plugins/- Project-level - •
~/.config/opencode/plugins/- Global
NPM Installation
Add to opencode.json:
{
"plugin": ["my-plugin", "@scope/custom-plugin"]
}
Dependencies
Local plugins can use npm packages. Create .opencode/package.json:
{
"dependencies": {
"shescape": "^2.1.0"
}
}
OpenCode runs bun install at startup to install dependencies.
Custom Tools
Basic Tool Definition
import { type Plugin, tool } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async (ctx) => {
return {
tool: {
mytool: tool({
description: "Tool description",
args: {
foo: tool.schema.string().describe("Parameter"),
},
async execute(args, ctx) {
return `Hello ${args.foo}!`
},
}),
},
}
}
Tool Naming Convention
- •Single export: Filename becomes tool name
- •Multiple exports:
<filename>_<exportname>(e.g.,math_add)
Tool Context
Tools receive:
- •
agent: Agent ID - •
sessionID: Current session ID - •
messageID: Current message ID
Multi-Tool File
import { tool } from "@opencode-ai/plugin"
export const add = tool({
description: "Add two numbers",
args: {
a: tool.schema.number().describe("First number"),
b: tool.schema.number().describe("Second number"),
},
async execute(args) {
return args.a + args.b
},
})
export const multiply = tool({
description: "Multiply two numbers",
args: {
a: tool.schema.number(),
b: tool.schema.number(),
},
async execute(args) {
return args.a * args.b
},
})
Common Patterns
Environment Protection
export const EnvProtection = async ({ project, client, $, directory, worktree }) => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "read" && output.args.filePath.includes(".env")) {
throw new Error("Do not read .env files")
}
},
}
}
Notifications
export const NotificationPlugin = async ({ project, client, $, directory, worktree }) => {
return {
event: async ({ event }) => {
if (event.type === "session.idle") {
await $`osascript -e 'display notification "Session completed!" with title "opencode"'`
}
},
}
}
Command Escaping
import { escape } from "shescape"
export const MyPlugin = async (ctx) => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "bash") {
output.args.command = escape(output.args.command)
}
},
}
}
Structured Logging
export const MyPlugin = async ({ client }) => {
await client.app.log({
service: "my-plugin",
level: "info",
message: "Plugin initialized",
extra: { foo: "bar" },
})
return {}
}
Log levels: debug, info, warn, error
Compaction Hooks
Add Context to Compaction
import type { Plugin } from "@opencode-ai/plugin"
export const CompactionPlugin: Plugin = async (ctx) => {
return {
"experimental.session.compacting": async (input, output) => {
output.context.push(`
## Custom Context
- Current task status
- Important decisions made
- Files being actively worked on
`)
},
}
}
Replace Compaction Prompt
import type { Plugin } from "@opencode-ai/plugin"
export const CustomCompactionPlugin: Plugin = async (ctx) => {
return {
"experimental.session.compacting": async (input, output) => {
output.prompt = `You are generating a continuation prompt for a multi-agent swarm session.
Summarize:
1. The current task and its status
2. Which files are being modified and by whom
3. Any blockers or dependencies between agents
4. The next steps to complete the work
`
},
}
}
TypeScript Support
Import Types
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
return {
// Type-safe implementations
}
}
Zod Schema
import { z } from "zod"
export default {
description: "Tool description",
args: {
param: z.string().describe("Parameter description"),
},
async execute(args, context) {
return "result"
},
}
Load Order
Plugins load in this order:
- •Global config (
~/.config/opencode/opencode.json) - •Project config (
opencode.json) - •Global plugin directory (
~/.config/opencode/plugins/) - •Project plugin directory (
.opencode/plugins/)
Duplicate npm packages with same name and version load once. Local and npm plugins with similar names load separately.
Publishing Plugins
Package Structure
{
"name": "opencode-my-plugin",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
},
"peerDependencies": {
"@opencode-ai/plugin": "*"
}
}
Build Process
# TypeScript npm install typescript @types/node npx tsc # Or use Bun bun build ./src/index.ts --outdir ./dist --target node
NPM Publishing
npm login npm publish
Users install via:
{
"plugin": ["opencode-my-plugin"]
}
Best Practices
Error Handling
Always throw descriptive errors:
"tool.execute.before": async (input, output) => {
if (someCondition) {
throw new Error("Descriptive error message")
}
}
Performance
- •Avoid expensive operations in synchronous hooks
- •Use
awaitfor async operations - •Cache expensive results
Security
- •Validate all input
- •Sanitize commands before execution
- •Never log sensitive data
- •Use environment variables for secrets
Compatibility
- •Test with multiple OpenCode versions
- •Provide fallbacks for missing features
- •Document required permissions
Debugging
Use structured logging:
await client.app.log({
service: "my-plugin",
level: "debug",
message: "Debug information",
extra: { input, output },
})
Troubleshooting
Plugin Not Loading
- •Verify file is in correct directory
- •Check TypeScript syntax
- •Review load order conflicts
- •Check permissions in
opencode.json
Dependencies Not Found
- •Create
.opencode/package.json - •Run
bun installmanually - •Verify npm package name
Hook Not Firing
- •Verify event name spelling
- •Check if event is supported
- •Ensure hook returns correctly