OpenCode plugins are TypeScript modules that register lifecycle hooks with the
OpenCode runtime. Each plugin is a self-contained .ts file in .opencode/plugins/.
| Plugin | Purpose | Hooks |
|---|---|---|
ccgs-hooks.ts |
Session lifecycle, commit validation, asset checks, agent logging, gap detection | session.created, session.idle, experimental.session.compacting, experimental.compaction.autocontinue, tool.execute.before, tool.execute.after |
drift-detector.ts |
Detects agent/skill/command template drift | session.created (scan), tool.execute.after (single-file check) |
changelog-generator.ts |
Generates CHANGELOG.md from conventional commits | session.idle (preview), tool.execute.before (command detection) |
Every plugin follows this pattern:
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async ({ project, client, directory, worktree }) => {
const projectRoot = directory || worktree || process.cwd()
return {
// Lifecycle hooks
event: async ({ event }) => {
if (event.type === "session.created") { /* ... */ }
},
// Tool hooks (before execution)
"tool.execute.before": async (input, output) => {
// Modify output.args to change tool behavior
// Throw to block the tool from executing
},
// Tool hooks (after execution)
"tool.execute.after": async (input, output) => {
// React to completed tool calls
},
// Compaction hooks
"experimental.session.compacting": async (input, output) => {
// Feed context into the compaction event
output.context.push("Additional context for the compacted session")
},
"experimental.compaction.autocontinue": async (input, output) => {
// Add instructions for the auto-continued session
},
}
}Fires on session lifecycle events: session.created, session.idle, server.instance.disposed.
Fires before any tool executes. Use to:
- Validate input parameters
- Block dangerous operations (push to protected branches)
- Log agent invocations
- Modify tool arguments
Throw an Error to prevent the tool from executing.
Fires after tool completion. Use to:
- Validate output/result files
- Detect file pattern changes (skill modifications)
- Log agent completions
Fires when context compression is about to occur. Use to inject recovery context:
"experimental.session.compacting": async (input, output) => {
output.context.push("Current task: implementing player movement system...")
}Fires after compaction when the session auto-continues. Use to guide the session to recover state.
All plugins should use a structured logger:
function createPluginLogger(client: any, service: string) {
const log = (level: string, message: string, extra?: any) => {
client?.app?.log({ body: { service, level, message, extra } }).catch(() => {})
}
return {
debug: (m: string, x?: any) => log("debug", m, x),
info: (m: string, x?: any) => log("info", m, x),
warn: (m: string, x?: any) => log("warn", m, x),
error: (m: string, x?: any) => log("error", m, x),
}
}- Create
{plugin-name}.tsin.opencode/plugins/ - Export a
Plugininstance (export const MyPlugin: Plugin = ...) - Register the plugin in
opencode.jsonunder thepluginsarray - Write tests in
.opencode/plugins/tests/following thetest-*.mjsnaming convention - Document the plugin in this README
- Always wrap hook handlers in try/catch
- Log errors via the plugin logger (don't throw from
eventhandlers) - Throw from
tool.execute.beforeonly to block a tool from executing - Use
logAudit()for persistent audit trail (ccgs-hooks utility) - Never throw from
tool.execute.after(tool already completed)
Tests are Node.js ESM scripts in .opencode/plugins/tests/. Each test:
- Imports the plugin's exported functions
- Passes simulated project context
- Asserts expected output
Run all plugin tests:
node .opencode/plugins/tests/test-*.mjsPlugins are configured in opencode.json:
{
"plugins": [
".opencode/plugins/ccgs-hooks.ts",
".opencode/plugins/drift-detector.ts",
".opencode/plugins/changelog-generator.ts"
]
}Plugins load in order. The first plugin's hooks fire first.