Install
$ agentstack add skill-timmy6942025-opencode-builder-skill-opencode-builder-skill ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ● Shell / process execution Used
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
OpenCode Builder
A comprehensive skill for building on top of OpenCode — an AI-powered coding assistant. This skill covers both the JavaScript/TypeScript SDK for programmatic control and the plugin architecture for extending OpenCode's behavior.
Table of Contents
- [Quick Start: Choose Your Path](#quick-start-choose-your-path)
- [SDK Path: Programmatic Control](#sdk-path-programmatic-control)
- [Plugin Path: Extending OpenCode](#plugin-path-extending-opencode)
- [Configuration Deep-Dive](#configuration-deep-dive)
- [Common Patterns](#common-patterns)
- [Advanced Orchestration](#advanced-orchestration)
- [Debugging & Troubleshooting](#debugging--troubleshooting)
- [Reference Files](#reference-files)
Quick Start: Choose Your Path
OpenCode offers two primary extension surfaces:
| Path | Use When | Key Packages | |------|----------|-------------| | SDK (@opencode-ai/sdk) | You want to control OpenCode from an external app, script, or service. Build integrations, automate workflows, or create UIs that interact with OpenCode. | @opencode-ai/sdk | | Plugin (@opencode-ai/plugin) | You want to run code inside OpenCode, hook into its lifecycle, add custom AI tools, or modify behavior. | @opencode-ai/plugin |
You can use both in the same project. A plugin can internally use the SDK client to call back into OpenCode.
SDK Path: Programmatic Control
Installation
npm install @opencode-ai/sdk
# or
bun add @opencode-ai/sdk
Two Client Modes
Mode 1: Full Lifecycle — starts a local server + client
import { createOpencode } from "@opencode-ai/sdk"
const { client, server } = await createOpencode({
hostname: "127.0.0.1",
port: 4096,
timeout: 5000, // ms to wait for server start
config: { // overrides / merges with opencode.json
model: "anthropic/claude-3-5-sonnet-20241022",
},
})
// Use client...
await server.close()
Mode 2: Client Only — connects to an already-running OpenCode server
import { createOpencodeClient } from "@opencode-ai/sdk"
const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
throwOnError: false, // return error objects instead of throwing
responseStyle: "fields", // or "data"
})
Core Workflows
1. Session Management
Sessions are the primary unit of interaction in OpenCode. Create them, send prompts, and read messages.
// Create a session
const session = await client.session.create({
body: { title: "My automation task" },
})
// Initialize a session (analyzes project and creates AGENTS.md context)
await client.session.init({ path: { id: session.id } })
// Send a prompt and get AI response
const result = await client.session.prompt({
path: { id: session.id },
body: {
parts: [{ type: "text", text: "Refactor the auth module" }],
},
})
// result.data.info contains the assistant message metadata
// result.data.parts contains message parts (text, tool calls, etc.)
// Inject context WITHOUT triggering a response (great for setup/context)
await client.session.prompt({
path: { id: session.id },
body: {
noReply: true,
parts: [{ type: "text", text: "You are working in a Next.js project." }],
},
})
// Run a shell command through the session
const shellResult = await client.session.shell({
path: { id: session.id },
body: { command: "npm test" },
})
// List all messages in a session
const messages = await client.session.messages({ path: { id: session.id } })
2. Structured Output
Request validated JSON from the model using JSON Schema:
const result = await client.session.prompt({
path: { id: sessionId },
body: {
parts: [{ type: "text", text: "Analyze this codebase" }],
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
summary: { type: "string" },
techStack: { type: "array", items: { type: "string" } },
entryPoints: { type: "array", items: { type: "string" } },
},
required: ["summary", "techStack"],
},
retryCount: 2, // retries if validation fails (default: 2)
},
},
})
console.log(result.data.info.structured_output)
// { summary: "...", techStack: ["React", "TypeScript"], entryPoints: [...] }
// Check for structured output errors
if (result.data.info.error?.name === "StructuredOutputError") {
console.error("Failed after", result.data.info.error.retries, "attempts")
}
Best practices for structured output:
- Provide clear
descriptionfields on schema properties — the model uses these - Keep schemas focused; deeply nested schemas are harder to fill correctly
- Increase
retryCountfor complex schemas, decrease for simple ones - Always handle
StructuredOutputErrorgracefully
3. File Operations
// Read a file
const file = await client.file.read({ query: { path: "src/index.ts" } })
// file.data.content has the raw text
// file.data.type is "raw" or "patch"
// Search for text across files
const matches = await client.find.text({
query: { pattern: "function.*handler" },
})
// matches.data is an array of { path, lines, line_number, submatches }
// Find files by name
const files = await client.find.files({
query: { query: "*.ts", type: "file", limit: 50 },
})
// Find directories
const dirs = await client.find.files({
query: { query: "components", type: "directory" },
})
// Get git status for tracked files
const status = await client.file.status()
4. TUI Control
Control the OpenCode terminal interface programmatically:
await client.tui.appendPrompt({ body: { text: "npm install lodash" } })
await client.tui.submitPrompt() // Press "enter"
await client.tui.showToast({
body: { message: "Done!", variant: "success" },
})
await client.tui.openHelp()
await client.tui.openSessions()
await client.tui.openModels()
await client.tui.executeCommand({ body: { command: "/clear" } })
5. Event Streaming
Subscribe to real-time events via Server-Sent Events:
const events = await client.event.subscribe()
for await (const event of events.stream) {
console.log(event.type, event.properties)
// event.type examples: "session.updated", "message.part.updated", "tool.execute.after"
}
6. Authentication
Set API keys for third-party providers that OpenCode will use:
await client.auth.set({
path: { id: "anthropic" },
body: { type: "api", key: "sk-ant-api03-..." },
})
7. Logging
Write structured log entries that appear in OpenCode's internal logs:
await client.app.log({
body: {
service: "my-integration",
level: "info", // debug | info | warn | error
message: "Deployment started",
extra: { env: "production", version: "1.2.3" },
},
})
SDK Type Safety
All types are generated from OpenCode's OpenAPI spec:
import type { Session, Message, Part, Config, Project } from "@opencode-ai/sdk"
Plugin Path: Extending OpenCode
Installation
npm install @opencode-ai/plugin
# or
bun add @opencode-ai/plugin
Plugin Basics
A plugin is a JS/TS module that exports a function receiving a context object and returning hooks:
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
// Initialization code runs here
console.log("Plugin loaded for project:", project?.name)
return {
// Hook implementations go here
}
}
Context object (ctx) properties:
| Property | Type | Description | |----------|------|-------------| | project | Project \| null | Current project metadata | | directory | string | Current working directory | | worktree | string | Git worktree path | | client | OpencodeClient | SDK client for calling back into OpenCode | | $ | BunShell | Bun's shell API for executing commands |
Loading Plugins
Plugins load from two sources:
1. Local files (auto-loaded at startup)
.opencode/plugins/— project-level~/.config/opencode/plugins/— global
2. npm packages (installed automatically via Bun)
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-helicone-session", "@my-org/custom-plugin"]
}
Load order: Global config → Project config → Global plugin dir → Project plugin dir
Dependencies for local plugins: Add a package.json to .opencode/:
{
"dependencies": {
"shescape": "^2.1.0"
}
}
OpenCode runs bun install at startup.
Custom Tools
Add AI-accessible tools that OpenCode can invoke:
import { Plugin, tool } from "@opencode-ai/plugin"
export const CustomToolsPlugin: Plugin = async (ctx) => {
return {
tool: {
deploy_to_vercel: tool({
description: "Deploy the current project to Vercel",
args: {
preview: tool.schema.boolean().optional(),
environment: tool.schema.enum(["production", "staging"]).optional(),
},
async execute(args, context) {
const { directory, worktree } = context
// args.preview, args.environment are typed and validated
const { $ } = ctx
const result = await $`cd ${directory} && vercel ${args.preview ? "--preview" : ""}`
return `Deployed! Output: ${result.stdout}`
},
}),
},
}
}
Tool precedence: If a plugin tool shares a name with a built-in tool, the plugin's version wins.
Tool schema helpers:
tool.schema.string()tool.schema.number()tool.schema.boolean()tool.schema.enum([...])tool.schema.array(itemSchema).optional()— make any field optional.describe("help text")— add field descriptions
Lifecycle Hooks
Shell / Tool Interception
return {
// Runs BEFORE any tool executes — modify args or block actions
"tool.execute.before": async (input, output) => {
if (input.tool === "read" && output.args.filePath.includes(".env")) {
throw new Error("Blocked: do not read .env files")
}
// Modify arguments
if (input.tool === "bash") {
const { escape } = await import("shescape")
output.args.command = escape(output.args.command)
}
},
// Runs AFTER tool execution — inspect or modify results
"tool.execute.after": async (input, output) => {
console.log(`Tool ${input.tool} finished with:`, output.result)
},
// Inject environment variables into ALL shell execution
"shell.env": async (input, output) => {
output.env.MY_API_KEY = process.env.MY_API_KEY
output.env.PROJECT_ROOT = input.cwd
},
}
Session Hooks
return {
"session.created": async ({ event }) => {
console.log("New session:", event.properties.sessionId)
},
"session.updated": async ({ event }) => {
// Session metadata changed
},
"session.idle": async ({ event }) => {
// AI finished responding — great time for notifications
},
"session.error": async ({ event }) => {
console.error("Session error:", event.properties.error)
},
}
Message Hooks
return {
"message.updated": async ({ event }) => {
// A message was modified
},
"message.part.updated": async ({ event }) => {
// Streaming update to a message part
},
"message.part.removed": async ({ event }) => {
// A part was deleted (e.g., user removed a tool call)
},
}
TUI Hooks
return {
"tui.prompt.append": async ({ event }) => {
// User added text to the prompt
},
"tui.toast.show": async ({ event }) => {
// A toast notification was shown
},
"tui.command.execute": async ({ event }) => {
// User executed a slash command
},
}
Compaction Hooks (Experimental)
Customize how session context is summarized when it gets too long:
return {
"experimental.session.compacting": async (input, output) => {
// Inject additional context into the compaction prompt
output.context.push(`
## Project State
- Currently working on: auth refactor
- Important decisions: using JWT, not sessions
- Active files: src/auth.ts, src/middleware.ts
`)
// OR replace the entire compaction prompt:
output.prompt = `
You are generating a continuation prompt for a multi-agent session.
Summarize:
1. Current task and status
2. Files being modified
3. Blockers or dependencies
4. Next steps
`
},
}
When output.prompt is set, it replaces the default compaction prompt entirely.
Event Hook Pattern
Generic event listener for any event type:
return {
event: async ({ event }) => {
if (event.type === "session.idle") {
await ctx.client.app.log({
body: { service: "my-plugin", level: "info", message: "Session completed" },
})
}
},
}
Logging from Plugins
Always use client.app.log() instead of console.log for structured, actionable logging:
await ctx.client.app.log({
body: {
service: "my-plugin",
level: "info",
message: "Plugin initialized",
extra: { version: "1.0.0" },
},
})
Levels: debug, info, warn, error
Configuration Deep-Dive
opencode.json Schema
{
"$schema": "https://opencode.ai/config.json",
"model": "anthropic/claude-3-5-sonnet-20241022",
"provider": {
"anthropic": {
"name": "Anthropic",
"api": "anthropic",
"url": "https://api.anthropic.com"
},
"openai": {
"name": "OpenAI",
"api": "openai",
"url": "https://api.openai.com/v1"
},
"custom": {
"name": "My Proxy",
"api": "openai",
"url": "http://localhost:18921/v1"
}
},
"plugin": [
"opencode-helicone-session",
"file:///Users/me/my-plugin/src/index.ts"
],
"mcp": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
}
}
}
Runtime note: OpenCode runs on Bun, so plugins can use Bun-native APIs like Bun.file(), Bun.write(), Bun.hash(), and top-level await without bundling.
Config locations (precedence: top to bottom):
~/.config/opencode/opencode.json— global defaults./opencode.json— project-specific overrides
Key fields:
model— default model ID (format:provider/model-id)provider— custom API endpoints, useful for proxies or self-hosted modelsplugin— npm package names orfile://paths to local pluginsmcp— Model Context Protocol server configurations
TypeScript Plugin tsconfig
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true
}
}
Common Patterns
Pattern 1: Plugin That Starts a Background Service
A plugin that auto-starts a proxy server if not already running:
import { Plugin } from "@opencode-ai/plugin"
import { spawn } from "node:child_process"
const PORT = 18921
const PROXY = `http://localhost:${PORT}`
async function startProxy(): Promise {
try {
const res = await fetch(`${PROXY}/health`, { signal: AbortSignal.timeout(1000) })
if (res.ok) return
} catch { /* not running */ }
const child = spawn("node", ["proxy.js", String(PORT)], {
stdio: "ignore",
detached: true,
})
child.unref()
// Wait for startup
for (let i = 0; i setTimeout(r, 500))
}
}
export const ServicePlugin: Plugin = async (ctx) => {
await startProxy()
return {
"shell.env": async (_input, output) => {
output.env.MY_PROXY_URL = PROXY
},
}
}
Pattern 2: Notification on Session Completion
import { Plugin }
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [Timmy6942025](https://github.com/Timmy6942025)
- **Source:** [Timmy6942025/opencode-builder-skill](https://github.com/Timmy6942025/opencode-builder-skill)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.