agentsclimarketplace

Opencode builder

Skill Timmy6942025/opencode-builder-skill/skills/opencode-builder

Kilo/agent skill for building OpenCode extensions, plugins, and integrations

Install
npx -y skills add Timmy6942025/opencode-builder-skill --skill opencode-builder

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use this skill when the user is building extensions, plugins, integrations, or custom developer tools on top of OpenCode (opencode.ai). This includes: creating plugins with custom AI tools and lifecycle hooks, using the SDK for programmatic session control and multi-agent workflows, building model providers or MCP integrations, configuring opencode.json, and implementing advanced orchestration like checkpoint/resume, parallel execution, or adversarial review. Trigger for any development work that extends or automates OpenCode. Do NOT trigger for general usage questions, troubleshooting, UI theming, or tool comparisons.

SKILL.md

29.1 KB, as published. Nobody here has run it

OpenCode Builder

πŸ“š Official Docs: For the latest information, always refer to the official documentation: For the latest SDK docs: https://opencode.ai/docs/sdk/. For plugins: https://opencode.ai/docs/plugins/.

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

  1. Quick Start: Choose Your Path
  2. SDK Path: Programmatic Control
  3. Plugin Path: Extending OpenCode
  4. Configuration Deep-Dive
  5. Common Patterns
  6. Advanced Orchestration
  7. Agent Skills
  8. Custom Commands
  9. Debugging & Troubleshooting
  10. Best Practices

Quick Start: Choose Your Path

OpenCode offers two primary extension surfaces:

PathUse WhenKey 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.

Decision Table

ScenarioRecommended Path
Build a CI/CD integrationSDK
Add a custom tool the AI can callPlugin
Create a webhook listener that triggers sessionsSDK
Intercept and modify tool callsPlugin
Build a dashboard showing session statusSDK
Protect sensitive files from being readPlugin
Automate multi-agent workflowsSDK + Plugin
Add notifications on session completionPlugin
Create an external UI for OpenCodeSDK
Inject environment variables into all shell commandsPlugin

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-sonnet-4-5",
  },
})

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

Response Styles

  • "fields" (default) β€” Each field is a separate result property
  • "data" β€” Nested data object

Error Handling

// With throwOnError: false (default)
const result = await client.session.create({ body: {} })
if (result.error) {
  console.error("Failed:", result.error)
}

// With throwOnError: true
const session = await client.session.create({
  body: {},
  throwOnError: true, // throws on error
})

Core Workflows

1. Session Management

Sessions are the primary unit of interaction in OpenCode.

// Create a session
const session = await client.session.create({
  body: { title: "My automation task" },
})

// Initialize (analyzes project, 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 β€” assistant message metadata
// result.data.parts β€” message parts (text, tool calls, etc.)

// Inject context WITHOUT triggering a response
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
const messages = await client.session.messages({ path: { id: session.id } })

// Get session children (subagent sessions)
const children = await client.session.children({ path: { id: session.id } })

// Get todo list
const todos = await client.session.todo({ path: { id: session.id } })

// Abort a running session
await client.session.abort({ path: { id: session.id } })

// Fork a session
const forked = await client.session.fork({
  path: { id: session.id },
  body: { messageID: "optional-msg-id" },
})

// Share / unshare
await client.session.share({ path: { id: session.id } })
await client.session.unshare({ path: { id: session.id } })

// Get diff
const diff = await client.session.diff({ path: { id: session.id } })

// Summarize session
await client.session.summarize({
  path: { id: session.id },
  body: { providerID: "anthropic", modelID: "claude-sonnet-4-5" },
})

// Revert a message
await client.session.revert({
  path: { id: session.id },
  body: { messageID: "msg-123" },
})

// Delete session
await client.session.delete({ 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,
    },
  },
})

console.log(result.data.info.structured_output)
// { summary: "...", techStack: ["React", "TypeScript"], entryPoints: [...] }

if (result.data.info.error?.name === "StructuredOutputError") {
  console.error("Failed after", result.data.info.error.retries, "attempts")
}

Best practices:

  • Provide clear description fields on schema properties
  • Keep schemas focused; deeply nested schemas are harder to fill
  • Increase retryCount for complex schemas
  • Always handle StructuredOutputError gracefully

3. File Operations

// Read a file
const file = await client.file.read({ query: { path: "src/index.ts" } })

// Search for text across files
const matches = await client.find.text({
  query: { pattern: "function.*handler" },
})

// 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" },
})

// Find workspace symbols
const symbols = await client.find.symbols({
  query: { query: "UserService" },
})

// Get git status for tracked files
const status = await client.file.status()

4. TUI Control

await client.tui.appendPrompt({ body: { text: "npm install lodash" } })
await client.tui.submitPrompt()
await client.tui.clearPrompt()
await client.tui.showToast({
  body: { message: "Done!", variant: "success" },
})
await client.tui.openHelp()
await client.tui.openSessions()
await client.tui.openModels()
await client.tui.openThemes()
await client.tui.executeCommand({ body: { command: "/clear" } })

5. Event Streaming

const events = await client.event.subscribe()
for await (const event of events.stream) {
  console.log(event.type, event.properties)
  // Types: "session.updated", "message.part.updated", "tool.execute.after", etc.
}

6. Authentication

await client.auth.set({
  path: { id: "anthropic" },
  body: { type: "api", key: "sk-ant-api03-..." },
})

7. Logging

await client.app.log({
  body: {
    service: "my-integration",
    level: "info",
    message: "Deployment started",
    extra: { env: "production", version: "1.2.3" },
  },
})

SDK Type Safety

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 }) => {
  console.log("Plugin loaded for project:", project?.name)

  return {
    // Hook implementations go here
  }
}

Context object properties:

PropertyTypeDescription
projectProject | nullCurrent project metadata
directorystringCurrent working directory
worktreestringGit worktree path
clientOpencodeClientSDK client for calling back into OpenCode
$BunShellBun's shell API for executing commands

Loading Plugins

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

All Plugin Hooks

Tool Hooks

return {
  // 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")
    }
    if (input.tool === "bash") {
      const { escape } = await import("shescape")
      output.args.command = escape(output.args.command)
    }
  },

  // AFTER tool execution β€” inspect or modify results
  "tool.execute.after": async (input, output) => {
    console.log(`Tool ${input.tool} finished:`, output.result)
  },
}

Shell Hooks

return {
  "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.idle": async ({ event }) => {
    // AI finished responding β€” great for notifications
  },
  "session.error": async ({ event }) => {
    console.error("Session error:", event.properties.error)
  },
  "session.compacted": async ({ event }) => {},
  "session.deleted": async ({ event }) => {},
  "session.diff": async ({ event }) => {},
  "session.status": async ({ event }) => {},
}

Message Hooks

return {
  "message.updated": async ({ event }) => {},
  "message.removed": async ({ event }) => {},
  "message.part.updated": async ({ event }) => {},
  "message.part.removed": async ({ event }) => {},
}

TUI Hooks

return {
  "tui.prompt.append": async ({ event }) => {},
  "tui.command.execute": async ({ event }) => {},
  "tui.toast.show": async ({ event }) => {},
}

File Hooks

return {
  "file.edited": async ({ event }) => {},
  "file.watcher.updated": async ({ event }) => {},
}

Permission Hooks

return {
  "permission.asked": async ({ event }) => {},
  "permission.replied": async ({ event }) => {},
}

LSP Hooks

return {
  "lsp.client.diagnostics": async ({ event }) => {},
  "lsp.updated": async ({ event }) => {},
}

Other Hooks

return {
  "command.executed": async ({ event }) => {},
  "installation.updated": async ({ event }) => {},
  "todo.updated": async ({ event }) => {},
  "server.connected": async ({ event }) => {},
}

Compaction Hooks (Experimental)

return {
  "experimental.session.compacting": async (input, output) => {
    // Inject additional context
    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
`
  },
}

Custom Tools via Plugins

import { type 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
          const { $ } = ctx
          const result = await $`cd ${directory} && vercel ${args.preview ? "--preview" : ""}`
          return `Deployed! Output: ${result.stdout}`
        },
      }),
    },
  }
}

Tool schema helpers:

  • tool.schema.string(), .number(), .boolean(), .enum([...]), .array(itemSchema)
  • .optional() β€” make any field optional
  • .describe("help text") β€” add field descriptions

Tool precedence: Plugin tools override built-in tools with the same name.

Standalone Custom Tools (.opencode/tools/)

You can also define custom tools as standalone TypeScript/JS files in .opencode/tools/. These are loaded automatically without needing a plugin wrapper:

.opencode/tools/
  my-tool.ts
  another-tool.js
// .opencode/tools/my-tool.ts
import { tool } from "@opencode-ai/plugin"

export default tool({
  description: "A standalone custom tool",
  args: {
    query: tool.schema.string().describe("Input query"),
  },
  async execute(args, context) {
    return `Result for: ${args.query}`
  },
})

Standalone tools are simpler for single-function tools where you don't need lifecycle hooks or plugin context. For more complex tools that need access to the plugin context ($, client, directory), use plugin-defined tools instead.

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:

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-sonnet-4-5",
  "small_model": "anthropic/claude-haiku-4-5",
  "provider": {
    "anthropic": {
      "options": {
        "timeout": 600000,
        "chunkTimeout": 30000
      }
    }
  },
  "plugin": ["opencode-helicone-session"],
  "mcp": {
    "filesystem": {
      "type": "local",
      "command": ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
    }
  },
  "tools": {
    "write": false,
    "bash": false
  },
  "permission": {
    "*": "allow",
    "bash": "ask"
  },
  "agent": {
    "code-reviewer": {
      "description": "Reviews code for best practices",
      "mode": "subagent",
      "model": "anthropic/claude-sonnet-4-5",
      "permission": { "edit": "deny" }
    }
  },
  "default_agent": "build",
  "share": "manual",
  "formatter": true,
  "lsp": true,
  "snapshot": true,
  "autoupdate": true,
  "instructions": ["CONTRIBUTING.md", "docs/guidelines.md"],
  "disabled_providers": [],
  "enabled_providers": [],
  "experimental": {},
  "attachment": {
    "image": {
      "auto_resize": true,
      "max_width": 2000,
      "max_height": 2000,
      "max_base64_bytes": 5242880
    }
  },
  "shell": "zsh",
  "server": {
    "port": 4096,
    "hostname": "0.0.0.0",
    "mdns": true,
    "cors": ["http://localhost:5173"]
  },
  "watcher": {
    "ignore": ["node_modules/**", "dist/**", ".git/**"]
  },
  "compaction": {
    "auto": true,
    "prune": true,
    "reserved": 10000
  }
}

Config Precedence (8 tiers, later wins)

  1. Remote config (.well-known/opencode)
  2. Global config (~/.config/opencode/opencode.json)
  3. Custom config (OPENCODE_CONFIG env var)
  4. Project config (opencode.json in project root)
  5. .opencode directories
  6. Inline config (OPENCODE_CONFIG_CONTENT env var)
  7. Managed config files (/Library/Application Support/opencode/ etc.)
  8. macOS managed preferences (.mobileconfig via MDM)

Configs are merged, not replaced. Non-conflicting settings from all configs are preserved.

Variable Substitution

{
  "model": "{env:OPENCODE_MODEL}",
  "provider": {
    "openai": {
      "options": {
        "apiKey": "{file:~/.secrets/openai-key}"
      }
    }
  }
}
  • {env:VARIABLE_NAME} β€” environment variable
  • {file:path/to/file} β€” file contents (relative to config or absolute)

tui.json

{
  "$schema": "https://opencode.ai/tui.json",
  "theme": "tokyonight",
  "leader_timeout": 2000,
  "keybinds": {
    "leader": "ctrl+x",
    "command_list": "ctrl+p"
  },
  "scroll_speed": 3,
  "scroll_acceleration": { "enabled": true },
  "diff_style": "auto",
  "mouse": true,
  "attention": {
    "enabled": true,
    "notifications": true,
    "sound": true,
    "volume": 0.4
  }
}

Common Patterns

Pattern 1: Background Service Plugin

import { Plugin } from "@opencode-ai/plugin"
import { spawn } from "node:child_process"

const PORT = 18921
const PROXY = `http://localhost:${PORT}`

async function startProxy(): Promise<void> {
  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()

  for (let i = 0; i < 10; i++) {
    try {
      const res = await fetch(`${PROXY}/health`, { signal: AbortSignal.timeout(1000) })
      if (res.ok) return
    } catch {}
    await new Promise(r => 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 } from "@opencode-ai/plugin"

export const NotificationPlugin: Plugin = async ({ client }) => {
  return {
    event: async ({ event }) => {
      if (event.type === "session.idle") {
        await client.app.log({
          body: { service: "notify", level: "info", message: "Session completed" },
        })
      }
    },
  }
}

Pattern 3: File Protection Plugin

import { Plugin } from "@opencode-ai/plugin"

const BLOCKED_PATTERNS = [/\.env/, /\.ssh/, /secret/i, /token/i]

export const FileProtectionPlugin: Plugin = async () => {
  return {
    "tool.execute.before": async (input, output) => {
      if (input.tool !== "read") return
      const path = output.args.filePath as string
      if (BLOCKED_PATTERNS.some(p => p.test(path))) {
        throw new Error(`Blocked: ${path} is on the sensitive files list`)
      }
    },
  }
}

Pattern 4: MCP Server Integration

{
  "mcp": {
    "filesystem": {
      "type": "local",
      "command": ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
    },
    "github": {
      "type": "local",
      "command": ["npx", "-y", "@modelcontextprotocol/server-github"]
    }
  }
}

MCP servers run as separate processes (stdio). Use MCP for wrapping CLIs, SaaS APIs, community tools. Use plugins for lifecycle hooks, custom logic, intercepting behavior.

Pattern 5: Custom Model Provider

{
  "provider": {
    "ollama": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Ollama Local",
      "options": { "baseURL": "http://localhost:11434/v1" },
      "models": {
        "llama3.2": { "name": "Llama 3.2" }
      }
    }
  },
  "model": "ollama/llama3.2"
}

Pattern 6: SDK + Plugin Combo

import { Plugin } from "@opencode-ai/plugin"

export const OrchestratorPlugin: Plugin = async ({ client }) => {
  return {
    "session.created": async ({ event }) => {
      const sessionId = event.properties.sessionId
      await client.session.init({ path: { id: sessionId } })
      await client.session.prompt({
        path: { id: sessionId },
        body: {
          noReply: true,
          parts: [{ type: "text", text: "This is a Next.js 14 App Router project." }],
        },
      })
    },
  }
}

Pattern 7: Usage Tracking

import { Plugin } from "@opencode-ai/plugin"
import { readFile, writeFile } from "node:fs/promises"

const USAGE_FILE = "/tmp/opencode-usage.json"

export const UsagePlugin: Plugin = async () => {
  return {
    "tool.execute.after": async (input, output) => {
      const data = JSON.parse(await readFile(USAGE_FILE, "utf-8").catch(() => '{"tools":{}}'))
      data.tools[input.tool] = (data.tools[input.tool] ?? 0) + 1
      await writeFile(USAGE_FILE, JSON.stringify(data))
    },
  }
}

Pattern 8: Environment Variable Injection

import { Plugin } from "@opencode-ai/plugin"

export const InjectEnvPlugin: Plugin = async () => {
  return {
    "shell.env": async (input, output) => {
      output.env.MY_API_KEY = "secret"
      output.env.PROJECT_ROOT = input.cwd
    },
  }
}

Advanced Orchestration

When building large-scale systems on top of OpenCode β€” multi-agent workflows, automated refactoring pipelines, deep research systems, or CI/CD integrations β€” you need patterns for orchestrating many OpenCode sessions as worker agents.

Key Concepts

ConceptWhat It Means on OpenCode
Session-as-AgentEach session.create() spawns an isolated agent with its own context, tools, and message history
External OrchestratorA standalone Node.js/Bun app using @opencode-ai/sdk to spawn and coordinate sessions
Plugin OrchestratorAn OpenCode plugin that hooks into session lifecycle and renders TUI dashboards
State ManagementSQLite (bun:sqlite) or file-based checkpoints β€” sessions are ephemeral
Parallel ExecutionPromise.all with concurrency limits (3–5 sessions recommended)
Checkpoint/ResumeSave step, results, and metadata after each phase; resume by reconciling completed agents
Adversarial ReviewSpawn multiple agents from different angles, synthesizer merges findings
Quality GatesValidate agent outputs with tests (session.shell()) before accepting

Quick Example: Parallel Code Review

import { createOpencodeClient } from "@opencode-ai/sdk"

const client = createOpencodeClient({ baseUrl: "http://localhost:4096" })

async function parallelReview(files: string[]): Promise<string[]> {
  const reviews = await Promise.all(
    files.map(async (file) => {
      const session = await client.session.create({ body: { title: `review-${file}` } })
      await client.session.init({ path: { id: session.id } })
      const result = await client.session.prompt({
        path: { id: session.id },
        body: {
          parts: [{ type: "text", text: `Review ${file} for bugs and style issues.` }],
        },
      })
      const text = result.data.parts
        ?.filter((p: any) => p.type === "text")
        .map((p: any) => p.text)
        .join("\n") || ""
      await client.session.delete({ path: { id: session.id } })
      return text
    })
  )
  return reviews
}

When to Use Orchestration

ScenarioApproach
Security audit across 200+ filesParallel review agents + synthesizer
Framework migration (React 17 β†’ 18)Parallel migration agents + test validation gates
Deep research with cross-checkingParallel researchers + adversarial verification + synthesis
CI/CD pipeline integrationExternal orchestrator + webhook triggers
Real-time TUI dashboardPlugin with session.idle hook + SQLite state

Agent Skills

Skills are reusable instruction sets that agents load on-demand. Create a SKILL.md file:

.opencode/skills/my-skill/SKILL.md
---
name: my-skill
description: Does something useful for the user
---

## What I do
- Step 1
- Step 2

## When to use me
Use this when the user asks about X.

Skills are discovered from:

  • .opencode/skills/<name>/SKILL.md (project)
  • ~/.config/opencode/skills/<name>/SKILL.md (global)
  • .claude/skills/<name>/SKILL.md (Claude-compatible)
  • .agents/skills/<name>/SKILL.md (agent-compatible)

Custom Commands

Create slash commands via markdown files:

.opencode/commands/test.md
---
description: Run tests with coverage
agent: build
model: anthropic/claude-haiku-4-5
---

Run the full test suite with coverage report and show any failures.
Focus on the failing tests and suggest fixes.

Placeholders: $ARGUMENTS, $1, $2, !`command`, @filename


Debugging & Troubleshooting

Plugin Not Loading

  1. Check file location: .opencode/plugins/ or ~/.config/opencode/plugins/
  2. Check for syntax errors β€” OpenCode silently skips broken plugins
  3. Verify exports: export const MyPlugin = async (ctx) => ({ ... })
  4. Use client.app.log() for debugging
  5. For npm plugins: ensure listed in opencode.json, restart OpenCode

SDK Connection Issues

  1. Verify server is running on expected port (default: 4096)
  2. Check baseUrl matches actual server URL
  3. Use client.global.health() to verify connectivity
  4. Increase timeout if server takes long to start

TypeScript Errors

  1. Ensure @opencode-ai/plugin is installed
  2. Import types: import type { Plugin } from "@opencode-ai/plugin"
  3. Use moduleResolution: "bundler" in tsconfig

Tool Not Appearing

  1. Plugin must return tool: { myTool: tool({ ... }) }
  2. Tool names must be unique (plugin tools override built-ins)
  3. Restart OpenCode after plugin changes

Hook Not Firing

  1. Verify event name is exactly correct (case-sensitive)
  2. Some hooks are experimental: experimental.session.compacting
  3. Add a log in the plugin body to confirm loading

Best Practices

  1. Use TypeScript β€” both packages provide full type definitions
  2. Use client.app.log() β€” never console.log in plugins; logs are structured and actionable
  3. Handle errors gracefully β€” plugins that throw can break OpenCode workflows
  4. Keep plugin initialization fast β€” OpenCode waits for all plugins to load at startup
  5. Use noReply: true for context injection β€” don't trigger unnecessary AI responses
  6. Validate tool args β€” the tool.schema.* helpers do runtime validation
  7. Be careful with tool.execute.before β€” throwing errors blocks the tool entirely
  8. Use the SDK inside plugins β€” the client in plugin context is the same SDK client
  9. Respect user config β€” read from opencode.json, don't hardcode credentials
  10. Document your plugin β€” include a README with setup instructions and opencode.json example
  11. Use config merging β€” don't overwrite entire config objects, let OpenCode merge
  12. Test plugins in isolation β€” use a fresh project directory for testing
  13. Use meaningful plugin names β€” npm package names should be opencode-<name>
  14. Clean up resources β€” close file handles, child processes, and connections in plugin cleanup

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.