A field-tested reference for building OpenCode plugins — SDK hooks, tool patterns, platform gotchas, and a starter template. Built from firsthand experience developing and debugging the Catenary plugin for OpenCode.
This is not a tutorial. It's a practitioner's reference: the gotchas that aren't documented anywhere, the SDK behaviors that are wrong in examples, and the patterns that actually work.
| File | Purpose |
|---|---|
docs/index.md |
Entry point and quick rules |
docs/pitfalls-index.md |
Start here. 9 canonical patterns for common problems — match your issue and execute |
docs/sdk-hooks.md |
Complete hook reference with verified signatures and input/output shapes |
docs/tool-patterns.md |
Tool development: schema format, execute signature, CLI execution, error handling |
docs/platform-gotchas.md |
Windows/WSL/Unix differences that affect plugin behavior |
docs/debugging.md |
Log locations, CLI commands, standalone testing, common error patterns |
references/starter-plugin.md |
Copy-and-modify plugin template with checklist |
references/sdk-types.d.ts |
The actual @opencode-ai/plugin type definitions (SDK v1.1.59) |
// 1. Use the subpath import — bare specifier is broken in v1.1.59
import { tool } from "@opencode-ai/plugin/tool"; // ✓ correct
import { tool } from "@opencode-ai/plugin"; // ✗ broken
// 2. Tool execute returns a plain string
async execute(args, ctx) {
return "## Result\n\nmarkdown content"; // ✓ correct
return { content: [{ type: "text", text: "..." }] }; // ✗ wrong
}
// 3. Tool args use tool.schema — not plain objects
args: {
name: tool.schema.string().describe("Name to look up"), // ✓
name: { type: "string", description: "..." }, // ✗ wrong
}
// 4. Hook signature is (input, output) — not (ctx, result)
"tool.execute.before": async (input, output) => { ... } // ✓ correct
"tool.execute.before": async (ctx, result) => { ... } // ✗ wrong
// 5. Plugins are NOT auto-discovered
// You must add them to opencode.json plugin array:
// { "plugin": ["./path/to/my-plugin.mjs"] }Only these hooks actually exist in @opencode-ai/plugin v1.1.59:
| Hook | When it fires |
|---|---|
tool.execute.before |
Before every tool (built-in and plugin) |
tool.execute.after |
After every tool (success or failure) |
experimental.session.compacting |
Before session compaction |
dispose |
When OpenCode exits or reloads |
chat.message |
On user message (read-only, cannot modify) |
Hooks that don't exist (commonly referenced but not in SDK):
session.created— use firsttool.execute.beforesession.deleted— usedisposecommand.execute.before— not exposedtool.definition— not a hook
Unknown hooks are silently ignored — no error, no warning.
See docs/pitfalls-index.md — it maps specific problem descriptions to diagnostic procedures:
- "plugin not loading" → §B
- "hook not firing" → §C
- "tool not showing up" → §D
- "tool crashes" → §E
- "output wrong" → §F
- "plugin on Windows" → §G
- "how do I register a plugin" → §H
- "debugging" → §I
While building Catenary, we hit:
session.createdhook doesn't exist → session never registered@opencode-ai/pluginbare import fails on Windows Node v24 → plugin silently failed to load- Tool returned
{content:[...]}→ LLM saw garbage output tool.execute.beforecalled with wrong signature → hooks silently ignored- Spawn used cmd.exe on Windows, not bash → CLI tools failed silently
- Unix socket IPC on Windows → daemon never reachable
- Plugin in
~/.config/opencode/plugin/→ not auto-discovered, silently ignored
All of these are documented in docs/pitfalls-index.md.
MIT