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
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 }
})