OpenCode Plugins
Guide for creating OpenCode plugins. Use when building plugins to extend OpenCode with custom hooks, tools, or integrations.
Plugin Location
Plugins are loaded from:
.opencode/plugin/- Project-level (preferred)~/.config/opencode/plugin/- Global
Basic Structure
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
// project: Current project information
// client: OpenCode SDK client for AI interaction
// $: Bun shell API for executing commands
// directory: Current working directory
// worktree: Git worktree path
return {
// Hook implementations
}
}
Available Hooks
Core Hooks
| Hook | Signature | Purpose |
|---|---|---|
event |
(input: { event: Event }) => Promise<void> |
Generic event handler for all events |
config |
(input: Config) => Promise<void> |
Modify configuration |
tool |
{ [key: string]: ToolDefinition } |
Register custom tools |
auth |
AuthHook |
Custom authentication providers |
Chat Hooks
| Hook | Signature | Purpose |
|---|---|---|
chat.message |
(input, output: { message: UserMessage; parts: Part[] }) => Promise<void> |
Modify user messages before sending |
chat.params |
(input, output: { temperature, topP, topK, options }) => Promise<void> |
Modify LLM parameters |
Tool Execution Hooks
| Hook | Signature | Purpose |
|---|---|---|
tool.execute.before |
(input: { tool, sessionID, callID }, output: { args }) => Promise<void> |
Intercept before tool runs |
tool.execute.after |
(input: { tool, sessionID, callID }, output: { title, output, metadata }) => Promise<void> |
Process after tool completes |
Permission Hooks
| Hook | Signature | Purpose |
|---|---|---|
permission.ask |
(input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void> |
Auto-approve/deny permissions |
Experimental Hooks
| Hook | Signature | Purpose |
|---|---|---|
experimental.chat.system.transform |
(input: {}, output: { system: string[] }) => Promise<void> |
Modify system prompt |
experimental.chat.messages.transform |
(input: {}, output: { messages }) => Promise<void> |
Transform message history |
experimental.session.compacting |
(input: { sessionID }, output: { context: string[]; prompt?: string }) => Promise<void> |
Add context during compaction |
experimental.text.complete |
(input: { sessionID, messageID, partID }, output: { text }) => Promise<void> |
Text completion |
Common Patterns
Inject Context into System Prompt
Best for project-specific reminders that should appear in every conversation:
export const ContextPlugin: Plugin = async () => {
const REMINDER = `
## Project Guidelines
- Always run tests before committing
- Use TypeScript strict mode
`.trim()
return {
"experimental.chat.system.transform": async (_input, output) => {
output.system.push(REMINDER)
}
}
}
Preserve Context During Compaction
Ensures important context survives session compaction:
export const CompactionPlugin: Plugin = async () => {
return {
"experimental.session.compacting": async (_input, output) => {
output.context.push("## Critical Context\n- Key fact 1\n- Key fact 2")
}
}
}
Custom Compaction Prompt
Replace the default compaction prompt entirely:
export const CustomCompactionPlugin: Plugin = async () => {
return {
"experimental.session.compacting": async (_input, output) => {
output.prompt = `You are summarizing a coding session.
Focus on: current task, files modified, next steps.`
}
}
}
Protect Sensitive Files
Prevent reading .env or other sensitive files:
export const EnvProtection: Plugin = async () => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "read" && output.args.filePath?.includes(".env")) {
throw new Error("Reading .env files is not allowed")
}
}
}
}
Send Notifications
Use macOS notifications for session events:
export const NotificationPlugin: Plugin = async ({ $ }) => {
return {
event: async ({ event }) => {
if (event.type === "session.idle") {
await $`osascript -e 'display notification "Done!" with title "OpenCode"'`
}
}
}
}
Custom Tools
Add project-specific tools:
import { tool } from "@opencode-ai/plugin"
export const CustomToolsPlugin: Plugin = async () => {
return {
tool: {
myTool: tool({
description: "Does something useful",
args: {
input: tool.schema.string(),
count: tool.schema.number().optional(),
},
async execute(args, ctx) {
return `Processed: ${args.input} (count: ${args.count ?? 1})`
},
}),
},
}
}
Event Types
Available events for the event hook:
Session Events
session.created- New session startedsession.compacted- Session was compactedsession.deleted- Session deletedsession.diff- Changes detectedsession.error- Error occurredsession.idle- Session became idlesession.status- Status changedsession.updated- Session updated
Message Events
message.updated- Message changedmessage.removed- Message deletedmessage.part.updated- Part of message changedmessage.part.removed- Part of message deleted
File Events
file.edited- File was editedfile.watcher.updated- File watcher detected change
Other Events
command.executed- Command raninstallation.updated- Installation changedlsp.client.diagnostics- LSP diagnostics receivedlsp.updated- LSP state changedpermission.replied- Permission answeredpermission.updated- Permission changedserver.connected- Server connectedtodo.updated- Todo list changedtool.execute.after- Tool finishedtool.execute.before- Tool starting
Package Setup
For TypeScript support, ensure .opencode/package.json has:
{
"dependencies": {
"@opencode-ai/plugin": "^1.0.203"
}
}
Then run bun install in the .opencode/ directory.
Best Practices
- Use TypeScript - Get type safety and better IDE support
- Keep plugins focused - One plugin per concern
- Use experimental hooks carefully - They may change
- Test locally first - Plugins run on every session
- Don't block - Hooks should be fast; use async for slow operations
- Handle errors gracefully - Throwing stops the hook chain