OpenCode SDK Development
Build custom tools and plugins using the OpenCode SDK packages. Use when creating custom tools, implementing tool schemas, building plugins, managing OpenCode sessions, using opencode-ai/sdk or opencode-ai/plugin, creating a tool.ts file, writing an execute handler, using tool.schema, ToolContext, createOpencode, plugin hooks, custom auth, automating OpenCode from a script, building a CI tool, or integrating external APIs. Keywords: Zod schema, execute handler, event streaming, session management, abort signal, tool discovery, multiple tools per file, tool factory.
npx skills add github:pantheon-org/tekhne --skill agentic-harness--opencode-toolkit--build-toolQuick Start: Custom Tool in 3 Steps
- Create
.opencode/tool/my-tool.ts:
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Brief description of what the tool does",
args: {
query: tool.schema.string().describe("Search query")
},
async execute({ query }, { abort }) {
return `Result for: ${query}`
}
})
- Restart OpenCode — tools are auto-discovered.
- Filename = tool name:
my-tool.ts→my-tool.
Mindset
@opencode-ai/plugin— Tools + Hooks: Add LLM-callable tools, intercept execution, react to events@opencode-ai/sdk— Client API: Control OpenCode from external scripts, session management
When to use: Adding new capabilities the agent doesn’t have, automating OpenCode from CI/scripts, or integrating external APIs as first-class tools.
When NOT to use: Built-in tool exists (client.tool.list()). Need shell (use $ from Bun). Logic belongs in system prompt (AGENTS.md). Need MCP.
Full decision guide in advanced-patterns.md.
Tool File Locations
.opencode/tool/*.ts # project-specific
~/.config/opencode/tool/*.ts # global
Multiple named exports per file → multiple tools: filename_exportname
Anti-Patterns
NEVER return non-string from execute. WHY: The tool runtime expects a string response; returning an object throws at runtime.
// BAD - throws at runtime
async execute({ query }) { return { results: [] } }
// GOOD - serialize first
async execute({ query }) { return JSON.stringify({ results: [] }) }
NEVER import { tool } from "@opencode-ai/sdk". WHY: The SDK package has no tool() export. The tool factory is only in @opencode-ai/plugin.
// BAD - import error at runtime
import { tool } from "@opencode-ai/sdk"
// GOOD
import { tool } from "@opencode-ai/plugin"
NEVER ignore the abort signal in long-running operations. WHY: Ignoring abort leaves processes dangling after the user cancels, consuming resources.
// BAD - dangling fetch after cancel
async execute({ url }) { return await fetch(url).then(r => r.text()) }
// GOOD - propagate abort
async execute({ url }, { abort }) { return await fetch(url, { signal: abort }).then(r => r.text()) }
Reference Files
- advanced-patterns.md — patterns, anti-patterns, SDK examples; sdk-api.md — SDK client API; plugin-api.md — plugin hooks
- basic-tool.ts — simple tool; full-plugin.ts — complete plugin
Run bun run opencode run "list tools" to verify your tool is discovered and named correctly.
---
description: custom tools opencode SDK plugin execute handler abort signal Zod schema session management event streaming
---
Eval Scenarios
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
No eval scenarios available.
Advanced SDK Patterns
Advanced patterns for building robust OpenCode tools and plugins.
Pattern: Tool with External HTTP API
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Query a REST API and return results",
args: {
endpoint: tool.schema.string().describe("API endpoint path (e.g. /users/123)"),
method: tool.schema.enum(["GET", "POST", "PUT", "DELETE"]).default("GET").describe("HTTP method"),
body: tool.schema.string().optional().describe("Request body as JSON string")
},
async execute({ endpoint, method, body }, { abort }) {
const BASE_URL = process.env.API_BASE_URL ?? "https://api.example.com"
const response = await fetch(`${BASE_URL}${endpoint}`, {
method,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.API_TOKEN}`
},
body: body ?? undefined,
signal: abort // Respect cancellation
})
if (!response.ok) {
return `Error: ${response.status} ${response.statusText}`
}
const data = await response.json()
return JSON.stringify(data, null, 2)
}
})
Pattern: Plugin with Audit Logging
import type { Plugin } from "@opencode-ai/plugin"
import { appendFileSync } from "fs"
const LOG_FILE = ".opencode/audit.log"
function log(entry: string) {
const timestamp = new Date().toISOString()
appendFileSync(LOG_FILE, `[${timestamp}] ${entry}\n`)
}
export const AuditPlugin: Plugin = async () => ({
"tool.execute.before": async (input) => {
log(`TOOL_START tool=${input.tool} sessionID=${input.sessionID}`)
},
"tool.execute.after": async (input, output) => {
const status = output.error ? "ERROR" : "SUCCESS"
log(`TOOL_END tool=${input.tool} status=${status}`)
}
})
Pattern: Plugin with Allowlist Enforcement
import type { Plugin } from "@opencode-ai/plugin"
const BLOCKED_TOOLS = ["bash", "file_write", "run_command"]
const ALLOWED_BASH_PATTERNS = [/^npm test/, /^npm run lint/, /^git status/]
export const SafetyPlugin: Plugin = async () => ({
"tool.execute.before": async (input) => {
if (BLOCKED_TOOLS.includes(input.tool)) {
throw new Error(`Tool '${input.tool}' is blocked by safety policy`)
}
// For bash specifically, check command allowlist
if (input.tool === "bash" && input.args?.command) {
const allowed = ALLOWED_BASH_PATTERNS.some(p => p.test(input.args.command))
if (!allowed) {
throw new Error(`Bash command not in allowlist: ${input.args.command}`)
}
}
}
})
Pattern: SDK Client for CI Automation
import { createOpencode } from "@opencode-ai/sdk"
async function runCITask(prompt: string) {
const { client } = await createOpencode({
hostname: "127.0.0.1",
port: 4096,
timeout: 30000
})
// Create a fresh session
const { data: session } = await client.session.create()
// Send prompt
await client.session.prompt({
path: { id: session.id },
body: { parts: [{ type: "text", text: prompt }] }
})
// Stream events until session completes
const events = await client.event.subscribe()
for await (const event of events.events) {
if (event.type === "session.completed" && event.data.id === session.id) {
break
}
if (event.type === "message.text") {
process.stdout.write(event.data.content ?? "")
}
}
// Retrieve final messages
const { data: messages } = await client.session.messages({
path: { id: session.id }
})
return messages
}
Pattern: Tool with File Persistence
import { tool } from "@opencode-ai/plugin"
import { readFileSync, writeFileSync, existsSync } from "fs"
const STATE_FILE = ".opencode/tool-state.json"
function loadState(): Record<string, unknown> {
if (!existsSync(STATE_FILE)) return {}
return JSON.parse(readFileSync(STATE_FILE, "utf8"))
}
function saveState(state: Record<string, unknown>) {
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2))
}
export default tool({
description: "Store and retrieve key-value pairs across sessions",
args: {
action: tool.schema.enum(["get", "set", "list"]).describe("Action to perform"),
key: tool.schema.string().optional().describe("Key name"),
value: tool.schema.string().optional().describe("Value to store (for set)")
},
async execute({ action, key, value }) {
const state = loadState()
switch (action) {
case "get":
return key ? String(state[key] ?? "not found") : "key required"
case "set":
if (!key || value === undefined) return "key and value required"
state[key] = value
saveState(state)
return `Stored ${key} = ${value}`
case "list":
return Object.keys(state).join(", ") || "no keys stored"
}
}
})
Pattern: Multi-Tool Plugin
When you need multiple tools in one plugin, use the tool key in the plugin return object:
import type { Plugin } from "@opencode-ai/plugin"
import { tool } from "@opencode-ai/plugin"
const searchTool = tool({
description: "Search project files",
args: { pattern: tool.schema.string().describe("Search pattern") },
async execute({ pattern }) { return `Searching for ${pattern}` }
})
const statusTool = tool({
description: "Get project status",
args: {},
async execute() { return "Project status: OK" }
})
export const ProjectPlugin: Plugin = async () => ({
tool: {
"project-search": searchTool,
"project-status": statusTool
}
})
Error Handling Best Practices
export default tool({
description: "Robust tool with error handling",
args: { input: tool.schema.string().describe("Input to process") },
async execute({ input }, { abort }) {
try {
if (abort.aborted) return "Operation cancelled"
const result = await dangerousOperation(input)
return result
} catch (error) {
// ALWAYS return a string, even on error
if (error instanceof Error) {
return `Error: ${error.message}`
}
return `Unexpected error: ${String(error)}`
}
}
})
Rules:
- Always return a string — never throw from
execute - Check
abort.abortedbefore starting expensive work - Wrap in try-catch and return error as string
- Use specific error messages the AI can act on
Custom Tool Schema Reference
Use tool.schema (Zod) for argument validation:
args: {
query: tool.schema.string().describe("Search query"),
path: tool.schema.string().optional().describe("File path"),
limit: tool.schema.number().min(1).max(100).default(10).describe("Max results"),
format: tool.schema.enum(["json", "text"]).describe("Output format"),
recursive: tool.schema.boolean().default(false).describe("Search recursively")
}
Tool Context
type ToolContext = {
sessionID: string // Current session ID
messageID: string // Current message ID
agent: string // Current agent identifier
abort: AbortSignal // Signal for cancellation
}
Plugin Development Reference
import type { Plugin } from "@opencode-ai/plugin"
const plugin: Plugin = async (input) => {
const { client, project, directory, worktree, $ } = input
return {
tool: { myTool: tool({ /* definition */ }) },
event: async ({ event }) => { /* handle events */ },
config: async (config) => { /* modify config */ },
"chat.message": async (input, output) => { /* modify messages */ },
"tool.execute.before": async (input, output) => { /* pre-processing */ },
"tool.execute.after": async (input, output) => { /* post-processing */ }
}
}
export default plugin
SDK Client Reference
import { createOpencode, createOpencodeClient } from "@opencode-ai/sdk"
// Create both client and server
const { client, server } = await createOpencode({
hostname: "127.0.0.1",
port: 4096,
timeout: 5000
})
// Or just the client (when server is already running)
const client = createOpencodeClient({
baseUrl: "http://127.0.0.1:4096"
})
Client API Categories
| Category | Methods |
|---|---|
client.session |
list, create, get, delete, prompt, messages, fork, share |
client.project |
list, current |
client.file |
list, read, status |
client.find |
text, files, symbols |
client.tool |
ids, list |
client.event |
subscribe (SSE streaming) |
client.mcp |
status, add |
client.tui |
appendPrompt, submitPrompt, showToast |
Session Management
const { data: sessions } = await client.session.list()
const { data: session } = await client.session.create()
const { data: response } = await client.session.prompt({
path: { id: sessionId },
body: { parts: [{ type: "text", text: "Your message here" }] }
})
Event Streaming
const result = await client.event.subscribe()
for await (const event of result.events) {
console.log("Event:", event.type, event.data)
}
Cross-Language Tool
import { tool } from "@opencode-ai/plugin"
import { $ } from "bun"
export default tool({
description: "Run Python analysis script",
args: { file: tool.schema.string().describe("File to analyze") },
async execute({ file }) {
return await $`python3 analyze.py ${file}`.text()
}
})
Troubleshooting
Tool not appearing:
- Verify file is in
.opencode/tool/or~/.config/opencode/tool/(singular) - Check file exports a valid tool definition
- Restart OpenCode to reload tools
Schema errors:
- Ensure all required args are provided
- Check type constraints (string vs number)
- Verify optional fields use
.optional()
Execution errors:
- Check
executereturns a string - Verify async operations complete
- Handle errors and return error messages as strings
OpenCode Plugin API Reference
Complete API reference for @opencode-ai/plugin.
Table of Contents
Tool Definition
tool()
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>
})
Parameters
| 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 |
ToolContext
type ToolContext = {
sessionID: string // Current session identifier
messageID: string // Current message identifier
agent: string // Active agent name
abort: AbortSignal // Cancellation signal
}
Return Value
The execute function must return a Promise<string>. The string is passed back to the AI as the tool result.
Schema Types
tool.schema is an alias for Zod. Use it to define argument types.
String
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")
}
Number
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")
}
Boolean
args: {
// Required boolean
enabled: tool.schema.boolean().describe("Enable feature"),
// Optional with default
verbose: tool.schema.boolean().default(false).describe("Verbose output")
}
Enum
args: {
// String enum
status: tool.schema.enum(["pending", "active", "done"]).describe("Status"),
// Native enum
level: tool.schema.nativeEnum(LogLevel).describe("Log level")
}
Arrays
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")
}
Objects
args: {
config: tool.schema.object({
host: tool.schema.string(),
port: tool.schema.number()
}).describe("Configuration object")
}
Unions
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")
}
Nullable / Optional
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()
}
Plugin System
Plugin Function
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 myPlugin
PluginInput
type 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
}
Plugin Hooks
event
Handle real-time server events.
{
event: async ({ event }) => {
// event.type: string
// event.data: any
console.log("Event:", event.type)
}
}
config
Modify configuration on load.
{
config: async (config) => {
// Modify config object
config.theme = "dark"
}
}
tool
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"
}
})
}
}
chat.message
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"
})
}
}
chat.params
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
}
}
permission.ask
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"
}
}
}
tool.execute.before
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()
}
}
}
tool.execute.after
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()
}
}
Auth Hooks
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"
}
}
}
}
}
]
}
}
Auth Method Types
API Method
{
type: "api",
label: string,
prompts?: Array<TextPrompt | SelectPrompt>,
authorize?: (inputs?: Record<string, string>) => Promise<
| { type: "success", key: string, provider?: string }
| { type: "failed" }
>
}
OAuth Method
{
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" }
>
}
Prompt Types
// 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 Definitions
ToolDefinition
type ToolDefinition = {
description: string
args: ZodRawShape
execute: (args: any, context: ToolContext) => Promise<string>
}
Hooks
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>
}
ProviderContext
type ProviderContext = {
source: "env" | "config" | "custom" | "api"
info: Provider
options: Record<string, any>
}
Multiple Tool Exports
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
Best Practices
- Descriptive Names: Tool names come from filenames - use clear, action-oriented names
- Detailed Descriptions: Write descriptions that help AI understand when to use the tool
- Validate Everything: Use Zod schemas to catch invalid inputs early
- Return Strings: Execute must always return a string (use JSON.stringify for objects)
- Handle Abort: Check context.abort for long-running operations
- Error Messages: Return helpful error messages as strings, don’t throw
OpenCode SDK API Reference
Complete API reference for @opencode-ai/sdk.
Table of Contents
- Initialization
- Session API
- Project API
- File API
- Find API
- Tool API
- Event API
- Config API
- Provider API
- MCP API
- TUI API
- PTY API
- Auth API
Initialization
createOpencode
Creates both an OpenCode client and starts a local server.
import { createOpencode } from "@opencode-ai/sdk"
const { client, server } = await createOpencode({
hostname?: string, // Default: "127.0.0.1"
port?: number, // Default: 4096
timeout?: number, // Default: 5000ms
config?: Config // Custom config overrides
})
// Clean up
server.close()
createOpencodeClient
Creates only the client (connects to existing server).
import { createOpencodeClient } from "@opencode-ai/sdk"
const client = createOpencodeClient({
baseUrl?: string, // Default: "http://127.0.0.1:4096"
directory?: string, // Working directory
throwOnError?: boolean, // Throw on API errors
responseStyle?: "data" | "fields" // Response format
})
Session API
client.session.*
list
List all sessions.
const { data, error } = await client.session.list()
// data: Session[]
create
Create a new session.
const { data, error } = await client.session.create({
body?: {
title?: string,
parentID?: string
}
})
// data: Session
get
Get a specific session.
const { data, error } = await client.session.get({
path: { id: string }
})
// data: Session
delete
Delete a session.
const { data, error } = await client.session.delete({
path: { id: string }
})
update
Update session properties.
const { data, error } = await client.session.update({
path: { id: string },
body: {
title?: string,
pinned?: boolean
}
})
prompt
Send a message to a session.
const { data, error } = await client.session.prompt({
path: { id: string },
body: {
parts: Part[], // Message content
model?: { // Optional model override
providerID: string,
modelID: string
},
noReply?: boolean // Don't trigger AI response
}
})
Part Types
type Part =
| { type: "text", text: string }
| { type: "file", path: string, content?: string }
| { type: "image", url: string }
promptAsync
Send message and return immediately (async processing).
const { data, error } = await client.session.promptAsync({
path: { id: string },
body: {
parts: Part[],
model?: { providerID: string, modelID: string }
}
})
messages
Get all messages for a session.
const { data, error } = await client.session.messages({
path: { id: string }
})
// data: Message[]
message
Get a specific message.
const { data, error } = await client.session.message({
path: { id: string, messageID: string }
})
// data: Message
command
Send a slash command.
const { data, error } = await client.session.command({
path: { id: string },
body: {
command: string // e.g., "/help", "/init"
}
})
shell
Execute a shell command.
const { data, error } = await client.session.shell({
path: { id: string },
body: {
command: string
}
})
status
Get session status (idle, busy, etc.).
const { data, error } = await client.session.status({
path: { id: string }
})
abort
Abort current session activity.
const { data, error } = await client.session.abort({
path: { id: string }
})
fork
Fork a session at a specific message.
const { data, error } = await client.session.fork({
path: { id: string },
body: {
messageID: string
}
})
children
Get child sessions (forks).
const { data, error } = await client.session.children({
path: { id: string }
})
share / unshare
Share or unshare a session.
const { data, error } = await client.session.share({
path: { id: string }
})
// Returns share URL
const { data, error } = await client.session.unshare({
path: { id: string }
})
diff
Get the git diff for session changes.
const { data, error } = await client.session.diff({
path: { id: string }
})
revert / unrevert
Revert or restore messages.
const { data, error } = await client.session.revert({
path: { id: string },
body: { messageID: string }
})
const { data, error } = await client.session.unrevert({
path: { id: string }
})
summarize
Generate session summary.
const { data, error } = await client.session.summarize({
path: { id: string }
})
todo
Get todo list for session.
const { data, error } = await client.session.todo({
path: { id: string }
})
init
Initialize AGENTS.md for project.
const { data, error } = await client.session.init({
path: { id: string }
})
Project API
client.project.*
list
List all projects.
const { data, error } = await client.project.list()
// data: Project[]
current
Get current project.
const { data, error } = await client.project.current()
// data: Project
File API
client.file.*
list
List files in a directory.
const { data, error } = await client.file.list({
query: {
path: string,
recursive?: boolean
}
})
read
Read file content.
const { data, error } = await client.file.read({
query: {
path: string
}
})
status
Get file status (git status).
const { data, error } = await client.file.status()
Find API
client.find.*
text
Search for text in files.
const { data, error } = await client.find.text({
query: {
query: string,
path?: string,
caseSensitive?: boolean,
regex?: boolean
}
})
files
Find files by name.
const { data, error } = await client.find.files({
query: {
query: string,
path?: string
}
})
symbols
Find code symbols.
const { data, error } = await client.find.symbols({
query: {
query: string
}
})
Tool API
client.tool.*
ids
Get list of all tool IDs.
const { data, error } = await client.tool.ids()
// data: string[]
list
Get tools with JSON schemas for a provider/model.
const { data, error } = await client.tool.list({
query: {
providerID: string,
modelID: string
}
})
Event API
client.event.*
subscribe
Subscribe to server-sent events.
const result = await client.event.subscribe()
for await (const event of result.events) {
// event.type: string
// event.data: any
console.log(event)
}
// Or with options
const result = await client.event.subscribe({
query: { sessionID?: string }
})
Event Types
session.createdsession.deletedsession.updatedmessage.createdmessage.updatedtool.startedtool.completedpermission.requested
Config API
client.config.*
get
Get current configuration.
const { data, error } = await client.config.get()
update
Update configuration.
const { data, error } = await client.config.update({
body: {
theme?: string,
model?: { providerID: string, modelID: string }
}
})
providers
List configured providers.
const { data, error } = await client.config.providers()
Provider API
client.provider.*
list
List available providers.
const { data, error } = await client.provider.list()
auth
Get authentication methods for providers.
const { data, error } = await client.provider.auth()
oauth.authorize
Start OAuth flow.
const { data, error } = await client.provider.oauth.authorize({
path: { provider: string }
})
oauth.callback
Handle OAuth callback.
const { data, error } = await client.provider.oauth.callback({
path: { provider: string },
query: { code: string, state?: string }
})
MCP API
client.mcp.*
status
Get MCP server status.
const { data, error } = await client.mcp.status()
add
Dynamically add MCP server.
const { data, error } = await client.mcp.add({
body: {
name: string,
command: string,
args?: string[],
env?: Record<string, string>
}
})
TUI API
client.tui.*
Control the terminal UI programmatically.
appendPrompt
Append text to the prompt.
const { data, error } = await client.tui.appendPrompt({
body: { text: string }
})
submitPrompt
Submit the current prompt.
const { data, error } = await client.tui.submitPrompt()
clearPrompt
Clear the prompt.
const { data, error } = await client.tui.clearPrompt()
showToast
Show a toast notification.
const { data, error } = await client.tui.showToast({
body: {
message: string,
type?: "info" | "success" | "warning" | "error"
}
})
openHelp / openSessions / openThemes / openModels
Open various dialogs.
await client.tui.openHelp()
await client.tui.openSessions()
await client.tui.openThemes()
await client.tui.openModels()
executeCommand
Execute a TUI command.
const { data, error } = await client.tui.executeCommand({
body: { command: string }
})
PTY API
client.pty.*
Manage pseudo-terminal sessions.
list
const { data, error } = await client.pty.list()
create
const { data, error } = await client.pty.create({
body: {
cols?: number,
rows?: number,
cwd?: string
}
})
get / update / remove / connect
await client.pty.get({ path: { id: string } })
await client.pty.update({ path: { id: string }, body: { cols, rows } })
await client.pty.remove({ path: { id: string } })
await client.pty.connect({ path: { id: string } })
Auth API
client.auth.*
set
Set authentication credentials.
const { data, error } = await client.auth.set({
path: { provider: string },
body: {
type: "key" | "oauth",
key?: string,
access?: string,
refresh?: string,
expires?: number
}
})
Type Imports
Import types directly from the SDK:
import type {
Session,
Message,
Part,
Project,
Model,
Provider,
Permission,
UserMessage,
Auth,
Config,
Event
} from "@opencode-ai/sdk"
Error Handling
const { data, error, response } = await client.session.get({
path: { id: "invalid" }
})
if (error) {
console.error("Error:", error)
console.error("Status:", response.status)
} else {
console.log("Session:", data)
}
// Or with throwOnError
try {
const client = createOpencodeClient({ throwOnError: true })
const { data } = await client.session.get({ path: { id: "test" } })
} catch (e) {
console.error("API Error:", e)
}