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
Complete API reference for @opencode-ai/plugin.
Create a custom tool with schema validation.
import { tool } from "@opencode-ai/plugin"
export default tool({
description: string,
args: ZodRawShape,
execute: (args: InferredArgs, context: ToolContext) => Promise<string>
})| Parameter | Type | Description |
|---|---|---|
description | string | Human-readable description shown to AI |
args | ZodRawShape | Zod schema defining tool arguments |
execute | Function | Async function that returns result string |
type ToolContext = {
sessionID: string // Current session identifier
messageID: string // Current message identifier
agent: string // Active agent name
abort: AbortSignal // Cancellation signal
}The execute function must return a Promise<string>. The string is passed back to the AI as the tool result.
tool.schema is an alias for Zod. Use it to define argument types.
args: {
// Required string
name: tool.schema.string().describe("User name"),
// Optional string
title: tool.schema.string().optional().describe("Title"),
// String with default
format: tool.schema.string().default("json").describe("Format"),
// String with constraints
email: tool.schema.string().email().describe("Email address"),
url: tool.schema.string().url().describe("URL"),
uuid: tool.schema.string().uuid().describe("UUID"),
// Min/max length
code: tool.schema.string().min(1).max(100).describe("Code snippet")
}args: {
// Required number
count: tool.schema.number().describe("Item count"),
// Integer
index: tool.schema.number().int().describe("Index"),
// With constraints
age: tool.schema.number().min(0).max(120).describe("Age"),
price: tool.schema.number().positive().describe("Price"),
// Optional with default
limit: tool.schema.number().default(10).describe("Limit")
}args: {
// Required boolean
enabled: tool.schema.boolean().describe("Enable feature"),
// Optional with default
verbose: tool.schema.boolean().default(false).describe("Verbose output")
}args: {
// String enum
status: tool.schema.enum(["pending", "active", "done"]).describe("Status"),
// Native enum
level: tool.schema.nativeEnum(LogLevel).describe("Log level")
}args: {
// String array
tags: tool.schema.array(tool.schema.string()).describe("Tags"),
// With constraints
ids: tool.schema.array(tool.schema.string()).min(1).max(10).describe("IDs")
}args: {
config: tool.schema.object({
host: tool.schema.string(),
port: tool.schema.number()
}).describe("Configuration object")
}args: {
// Literal union
format: tool.schema.union([
tool.schema.literal("json"),
tool.schema.literal("xml"),
tool.schema.literal("csv")
]).describe("Output format"),
// Type union
value: tool.schema.union([
tool.schema.string(),
tool.schema.number()
]).describe("String or number value")
}args: {
// Optional (can be undefined)
comment: tool.schema.string().optional(),
// Nullable (can be null)
parent: tool.schema.string().nullable(),
// Both
value: tool.schema.string().optional().nullable()
}Plugins are async functions that return hooks.
import type { Plugin, Hooks } from "@opencode-ai/plugin"
const myPlugin: Plugin = async (input) => {
// input contains client, project, directory, worktree, $
const hooks: Hooks = {
// Define hooks here
}
return hooks
}
export default myPlugintype PluginInput = {
client: OpencodeClient // SDK client instance
project: Project // Current project info
directory: string // Plugin directory path
worktree: string // Git worktree path
$: BunShell // Bun shell for commands
}Handle real-time server events.
{
event: async ({ event }) => {
// event.type: string
// event.data: any
console.log("Event:", event.type)
}
}Modify configuration on load.
{
config: async (config) => {
// Modify config object
config.theme = "dark"
}
}Register custom tools.
{
tool: {
myTool: tool({
description: "My tool",
args: { input: tool.schema.string() },
async execute({ input }) {
return `Processed: ${input}`
}
}),
anotherTool: tool({
description: "Another tool",
args: {},
async execute() {
return "Done"
}
})
}
}Modify messages before sending to LLM.
{
"chat.message": async (input, output) => {
// input: { sessionID, agent?, model?, messageID? }
// output: { message: UserMessage, parts: Part[] }
// Modify parts
output.parts.push({
type: "text",
text: "Additional context"
})
}
}Modify LLM parameters.
{
"chat.params": async (input, output) => {
// input: { sessionID, agent, model, provider, message }
// output: { temperature, topP, options }
output.temperature = 0.7
output.topP = 0.9
output.options.maxTokens = 4096
}
}Handle permission requests.
{
"permission.ask": async (input, output) => {
// input: Permission object
// output: { status: "ask" | "deny" | "allow" }
if (input.tool === "dangerous_tool") {
output.status = "deny"
} else {
output.status = "allow"
}
}
}Pre-process tool arguments.
{
"tool.execute.before": async (input, output) => {
// input: { tool, sessionID, callID }
// output: { args: any }
if (input.tool === "myTool") {
output.args.timestamp = Date.now()
}
}
}Post-process tool output.
{
"tool.execute.after": async (input, output) => {
// input: { tool, sessionID, callID }
// output: { title, output, metadata }
output.title = `[${input.tool}] ${output.title}`
output.metadata.processedAt = Date.now()
}
}Custom authentication providers.
{
auth: {
provider: "my-provider",
// Optional: Custom credential loader
loader: async (auth, provider) => {
const credentials = await auth()
return { apiKey: credentials.key }
},
methods: [
// API key method
{
type: "api",
label: "API Key",
prompts: [
{
type: "text",
key: "apiKey",
message: "Enter your API key",
placeholder: "sk-...",
validate: (value) => {
if (!value.startsWith("sk-")) {
return "Invalid key format"
}
}
}
],
authorize: async (inputs) => {
return {
type: "success",
key: inputs!.apiKey
}
}
},
// OAuth method
{
type: "oauth",
label: "Sign in with OAuth",
prompts: [
{
type: "select",
key: "region",
message: "Select region",
options: [
{ label: "US", value: "us", hint: "United States" },
{ label: "EU", value: "eu", hint: "Europe" }
]
}
],
authorize: async (inputs) => {
const authUrl = `https://oauth.example.com/authorize?region=${inputs?.region}`
return {
url: authUrl,
instructions: "Complete sign-in in browser",
method: "auto",
callback: async () => {
// Poll for completion
return {
type: "success",
key: "obtained-api-key"
}
}
}
}
}
]
}
}{
type: "api",
label: string,
prompts?: Array<TextPrompt | SelectPrompt>,
authorize?: (inputs?: Record<string, string>) => Promise<
| { type: "success", key: string, provider?: string }
| { type: "failed" }
>
}{
type: "oauth",
label: string,
prompts?: Array<TextPrompt | SelectPrompt>,
authorize: (inputs?: Record<string, string>) => Promise<AuthOAuthResult>
}
type AuthOAuthResult = {
url: string,
instructions: string,
method: "auto" | "code",
callback: (code?: string) => Promise<
| { type: "success", provider?: string } & (
| { refresh: string, access: string, expires: number }
| { key: string }
)
| { type: "failed" }
>
}// Text input
{
type: "text",
key: string,
message: string,
placeholder?: string,
validate?: (value: string) => string | undefined,
condition?: (inputs: Record<string, string>) => boolean
}
// Select dropdown
{
type: "select",
key: string,
message: string,
options: Array<{
label: string,
value: string,
hint?: string
}>,
condition?: (inputs: Record<string, string>) => boolean
}type ToolDefinition = {
description: string
args: ZodRawShape
execute: (args: any, context: ToolContext) => Promise<string>
}interface Hooks {
event?: (input: { event: Event }) => Promise<void>
config?: (input: Config) => Promise<void>
tool?: { [key: string]: ToolDefinition }
auth?: AuthHook
"chat.message"?: (input: ChatMessageInput, output: ChatMessageOutput) => Promise<void>
"chat.params"?: (input: ChatParamsInput, output: ChatParamsOutput) => Promise<void>
"permission.ask"?: (input: Permission, output: PermissionOutput) => Promise<void>
"tool.execute.before"?: (input: ToolExecuteInput, output: ToolBeforeOutput) => Promise<void>
"tool.execute.after"?: (input: ToolExecuteInput, output: ToolAfterOutput) => Promise<void>
}type ProviderContext = {
source: "env" | "config" | "custom" | "api"
info: Provider
options: Record<string, any>
}Export multiple tools from one file:
// file: mytools.ts
import { tool } from "@opencode-ai/plugin"
export const search = tool({
description: "Search files",
args: { query: tool.schema.string() },
async execute({ query }) { /* ... */ }
})
export const replace = tool({
description: "Replace text",
args: {
search: tool.schema.string(),
replace: tool.schema.string()
},
async execute({ search, replace }) { /* ... */ }
})
// Creates tools: mytools_search, mytools_replace