Complete toolkit for configuring and extending OpenCode: agent creation, custom slash commands, configuration management, plugin development, and SDK usage.
75
94%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
All hook implementation patterns with examples
Listen to all events, discriminate by type:
return {
event: async ({ event }) => {
switch (event.type) {
case "session.idle":
console.log("Session completed:", event.properties.sessionID)
break
case "file.edited":
console.log("File changed:", event.properties.file)
break
}
},
}Register tools the LLM can call:
import { type Plugin, tool } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async (ctx) => {
return {
tool: {
lint: tool({
description: "Run ESLint on a file",
args: {
file: tool.schema.string().describe("File path to lint"),
fix: tool.schema.boolean().optional().describe("Auto-fix issues"),
},
async execute(args, context) {
const result = await ctx.$`eslint ${args.fix ? "--fix" : ""} ${args.file}`.quiet()
return result.text()
},
}),
},
}
}Intercept before/after tool execution:
return {
// Modify args or throw to block
"tool.execute.before": async (input, output) => {
if (input.tool === "read" && output.args.filePath?.includes(".env")) {
throw new Error("Reading .env files is blocked")
}
},
// Modify output/title/metadata
"tool.execute.after": async (input, output) => {
console.log(`Tool ${input.tool} completed`)
// Modify: output.title, output.output, output.metadata
},
}Override permission decisions:
return {
"permission.ask": async (input, output) => {
// input: { id, type, pattern, sessionID, messageID, title, metadata }
// output.status: "ask" | "deny" | "allow"
if (input.type === "bash" && input.metadata.command?.includes("rm -rf")) {
output.status = "deny"
}
},
}Modify messages or LLM parameters:
return {
// Intercept user messages
"chat.message": async (input, output) => {
// input: { sessionID, agent?, model?, messageID? }
// output: { message: UserMessage, parts: Part[] }
console.log("User message:", output.message)
},
// Modify LLM parameters per request
"chat.params": async (input, output) => {
// input: { sessionID, agent, model, provider, message }
// output: { temperature, topP, topK, options }
if (input.agent === "creative") {
output.temperature = 0.9
}
},
}Add custom provider authentication:
return {
auth: {
provider: "my-provider",
methods: [
{
type: "api",
label: "API Key",
prompts: [
{
type: "text",
key: "apiKey",
message: "Enter your API key",
validate: (v) => (v.length < 10 ? "Key too short" : undefined),
},
],
async authorize(inputs) {
return { type: "success", key: inputs!.apiKey }
},
},
],
},
}Customize session compaction:
return {
"experimental.session.compacting": async (input, output) => {
// Add context to default prompt
output.context.push("Remember: user prefers TypeScript")
// OR replace entire prompt
output.prompt = "Summarize this session focusing on code changes..."
},
}Modify configuration on load:
return {
config: async (config) => {
// Mutate config object
config.theme = "dark"
},
}| Hook | Signature | Mutate |
|---|---|---|
event | ({ event }) => void | Read-only |
config | (config) => void | Mutate config |
tool | Object of tool() definitions | N/A |
auth | AuthHook object | N/A |
chat.message | (input, output) => void | Mutate output |
chat.params | (input, output) => void | Mutate output |
permission.ask | (input, output) => void | Set output.status |
tool.execute.before | (input, output) => void | Mutate output.args |
tool.execute.after | (input, output) => void | Mutate output |
experimental.* | (input, output) => void | Mutate output |
Before creating any plugin, regenerate the API reference to ensure accuracy:
bun run .opencode/skill/opencode-build-plugins/scripts/extract-plugin-api.tsThis generates references/hooks.md, references/events.md, references/tool-helper.md.
Feasible as plugins:
NOT feasible (inform user):
If not feasible, inform user clearly. Suggest:
packages/opencodeFollow modular design principles from CODING-TS.MD:
index.tsFor complex plugins, use a modular directory structure:
.opencode/plugins/my-plugin/
├── index.ts # Entry point, exports Plugin
├── types.ts # TypeScript types/interfaces
├── utils.ts # Shared utilities
├── hooks/ # Hook implementations
│ ├── event.ts
│ └── tool-execute.ts
└── tools/ # Custom tool definitions
└── my-tool.tsExample modular index.ts:
import type { Plugin } from "@opencode-ai/plugin"
import { eventHooks } from "./hooks/event"
import { toolHooks } from "./hooks/tool-execute"
import { customTools } from "./tools"
export const MyPlugin: Plugin = async ({ project, client }) => {
return {
...eventHooks({ client }),
...toolHooks({ client }),
tool: customTools,
}
}import type { Plugin } from "@opencode-ai/plugin"
const BLOCKED = ["rm -rf", "git push --force", "DROP TABLE"]
export const SecurityPlugin: Plugin = async () => ({
"tool.execute.before": async (input) => {
if (input.tool === "bash") {
const cmd = input.args?.command ?? ""
if (BLOCKED.some((pattern) => cmd.includes(pattern))) {
throw new Error(`Blocked dangerous command: ${cmd}`)
}
}
}
})import type { Plugin } from "@opencode-ai/plugin"
import { appendFileSync } from "fs"
export const AuditPlugin: Plugin = async ({ project }) => ({
"tool.execute.after": async (input, output) => {
const entry = JSON.stringify({
ts: new Date().toISOString(),
tool: input.tool,
args: input.args,
success: !output.error
})
appendFileSync(".opencode/audit.log", entry + "\n")
}
})import type { Plugin } from "@opencode-ai/plugin"
export const NotifyPlugin: Plugin = async ({ client }) => ({
event: async ({ event }) => {
if (event.type === "session.completed") {
await client.tui.showToast({
message: "Session complete",
variant: "success"
})
}
}
})import type { Plugin } from "@opencode-ai/plugin"
import { tool } from "@opencode-ai/plugin"
const lookupUser = tool({
description: "Look up a user by email in the internal directory",
args: {
email: tool.schema.string().describe("User email address")
},
async execute({ email }) {
const result = await fetch(`https://api.example.com/users?email=${email}`)
return JSON.stringify(await result.json())
}
})
export const DirectoryPlugin: Plugin = async () => ({
tool: { "lookup-user": lookupUser }
})