Complete toolkit for configuring and extending OpenCode: agent creation, custom slash commands, configuration management, plugin development, and SDK usage.
98
98%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
Plugins live in .opencode/plugins/<name>/index.ts (project) or ~/.config/opencode/plugins/<name>/index.ts (global).
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
return {
// Add hooks here — see references/hooks.md for available hooks
}
}Register in opencode.json:
{ "plugin": ["file:///.opencode/plugins/my-plugin/index.ts"] }Optional — document plugin purpose in a companion agent file:
---
description: Plugin guard — intercepts tool calls, validates bash commands, blocks dangerous deletions, logs audit trail, hooks session events
---Then test: opencode run hi
Plugins are async factory functions returning hook implementations. They run once on load. Hooks fire at lifecycle points (tool execution, events, config load). Think of them as middleware layers.
Use tool.execute.before to intercept/block, tool.execute.after to log, event: for file edits/session end, auth: for custom model auth, chat.params: to modify LLM params, tool: key to add callable tools.
When to use: Intercept/log tool calls, add custom auth, react to lifecycle events, or extend OpenCode with new callable tools.
When NOT to use: Built-in tool exists (check client.tool.list()). Only need a shortcut (use slash command). Need behavior guidance (use AGENTS.md). Need MCP (use Model Context Protocol).
Verify plugin is loaded: run bun run opencode run "list your tools" and confirm the plugin output appears.
NEVER call client.registerTool() — it does not exist. WHY: The SDK has no such method; calling it throws at runtime.
// BAD - runtime error
client.registerTool("my-tool", { ... })
// GOOD - return the tool from the factory
export const Plugin: Plugin = async ({ client }) => ({
tool: { "my-tool": myTool }
})NEVER write sync hook handlers. WHY: The plugin loader validates async signatures; a sync handler causes the entire plugin to be rejected at load time with a type error.
// BAD - rejected at load
"tool.execute.before": (input) => { log(input) }
// GOOD - always async
"tool.execute.before": async (input) => { log(input) }NEVER mutate input.args in tool.execute.before to block a tool — the input is read-only and silently ignored. WHY: Mutations have no effect; to block execution you must throw.
// BAD - silently ignored
"tool.execute.before": async (input) => { input.args.command = "echo safe" }
// GOOD - throw to block
"tool.execute.before": async (input) => {
if (isDangerous(input.args)) throw new Error("Blocked")
}See references/hook-patterns.md for complete anti-pattern list + full hook/event/tool/UI/testing/publishing reference.