skills/opencode-plugin-creator/SKILL.md
Guide for creating OpenCode plugins — hook plugins (.ts), npm plugins, and TUI plugins (.tsx). Use when building, updating, or packaging OpenCode plugins.
npx skillsauth add Thomashighbaugh/opencode opencode-plugin-creatorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Guide for creating OpenCode plugins. Covers three plugin types:
.ts files that register lifecycle hooksopencode.jsoncAsk the user: "What behavior are you trying to add?"
| If they want to... | Use hook(s) |
|-------------------|-------------|
| Auto-approve safe bash commands | permission.ask |
| Persist state across compaction | experimental.session.compacting |
| Inject context every turn | experimental.chat.system.transform |
| Detect magic keywords in chat | chat.message or tui.prompt.append |
| Track tool usage | tool.execute.before / .after |
| Intercept custom commands | command.execute.before / .after |
| Add TUI dialog menus | TUI plugin with DialogSelect |
| Run on session start/end | session.created / .deleted |
| Modify tool arguments before execution | tool.execute.before (mutate input) |
| Type | Location | Registration | Best For |
|------|----------|-------------|----------|
| Hook plugin (.ts) | plugins/my-plugin.ts | "./plugins/my-plugin.ts" | Internal hooks, project-specific behavior |
| NPM plugin | node_modules/ | "@scope/package@version" | Reusable, published behavior |
| TUI plugin (.tsx) | plugins/my-tui/ | "./plugins/my-tui/index.js" | Visual dialogs and menus |
Use the templates in references/templates.md to scaffold:
# Hook plugin (single file)
touch plugins/my-plugin.ts
# Paste the Minimal Hook Plugin Template
# TUI plugin (project)
mkdir -p plugins/my-tui/src plugins/my-tui/dist
# Create package.json, src/index.tsx, build config
Each hook follows this pattern:
hooks["hook.name"] = async (input, output) => {
// input contains event data
// output allows modification of behavior
// For context injection: use queueContextMessage() + experimental.chat.system.transform
// For state: use readJsonFile()/writeJsonFile() in .opencode/state/sessions/<id>/
// For permissions: set output.status = 'allow' | 'deny'
// For compaction: push to output.context[]
// For system: push to output.system[]
}
Add the plugin to opencode.jsonc:
{
"plugin": [
"./plugins/my-plugin.ts",
]
}
# Check plugin loads
btw_status
# Run hub doctor for diagnostics
/hubs-doctor
# Verify hooks fire by observing behavior
OpenCode has two independent mechanisms for injecting context:
experimental.chat.system.transform)const _contextMessages: Record<string, string[]> = {}
function queueContextMessage(sessionId: string, message: string) {
if (!_contextMessages[sessionId]) _contextMessages[sessionId] = []
_contextMessages[sessionId].push(message)
}
function consumeContextMessages(sessionId: string): string[] {
const messages = _contextMessages[sessionId] || []
delete _contextMessages[sessionId]
return messages
}
hooks["experimental.chat.system.transform"] = async (input, output) => {
const sessionId = input.sessionID
if (!sessionId) return
const messages = consumeContextMessages(sessionId)
if (messages.length === 0) return
// Wrapped in XML tags for clean parsing
output.system.push(`<my-plugin>\n${messages.join('\n\n')}\n</my-plugin>`)
}
Key detail: Messages queued via queueContextMessage() are injected into the system prompt on every turn. They persist until consumed. Use this for: mode reminders, state summaries, todo lists.
experimental.session.compacting)hooks["experimental.session.compacting"] = async (_input, output) => {
// Push plain markdown — survives context window compression
output.context.push(`## My Plugin State
- Active: true
- Started: ${startedAt}
- Items processed: ${count}
`)
}
Key detail: output.context is markdown injected during context window compression events. It survives compaction and ensures the agent remembers plugin state after the window is compressed.
State files live in .opencode/state/ (gitignored):
.opencode/state/
├── my-plugin-state.json # Session-independent state
└── sessions/<sessionId>/
└── my-plugin-state.json # Per-session state (auto-cleaned)
Use per-session state by passing sessionId to the file path. Use global state for cross-session data.
hooks["permission.ask"] = async (input, output) => {
if (input.type !== 'bash') return
const command = typeof input.pattern === 'string' ? input.pattern : ''
// Define safe command patterns
const safePatterns = [
/^git (status|diff|log|branch|show|fetch)/,
/^npm (test|run (test|lint|build|check))/,
/^ls( |$)/,
/^node --check/,
]
const isSafe = safePatterns.some(p => p.test(command.trim()))
const hasDangerousChars = /[;&|`$()<>\n\r\t\0\\]/.test(command)
if (isSafe && !hasDangerousChars) {
output.status = 'allow'
}
// Leave unset = prompt user (default)
}
// plugins/<name>/src/index.tsx
import { Plugin } from "@opencode-ai/tui-sdk"
export default {
async setup(client) {
client.registerDialog("my-command", {
render: ({ onClose }) => (
<MyHubDialog options={[...]} onClose={onClose} />
)
})
}
}
Build with bun run build (configured in package.json). Register compiled .js in opencode.jsonc.
references/hook-api.md — Complete hook API reference and patternsreferences/templates.md — Ready-to-use templates for hook, stateful, and TUI plugins~/.config/opencode/plugins/ for referenceplugins/hubs-plugin.ts — Full-featured hook plugin exampleplugins/hubs-tui/ — Full TUI plugin exampleopencode.jsonc — Plugin registration configtools
Create valid, type-safe TypeScript tools for OpenCode — generates correct boilerplate, typed interfaces, and handler stubs. Use when building new tools for global config or per-project .opencode/tools/.
testing
Explore multiple solution branches in parallel, evaluate each, and recommend the best path. Use when the user explicitly invokes /ideation tree-of-thoughts or asks for "tree of thought" / "branching exploration".
testing
Run multiple independent reasoning passes and find consensus. Use when the decision is critically important, the stakes are high, or the user explicitly requests "run it multiple times" / "check consistency" / "self-consistency".
testing
Optimization by PROmpting — generate candidate prompt variations, test each against a benchmark, and report the best performer. Use when the user invokes /ideation opro or asks for "prompt optimization" / "OPRO".