Opencode plugin development
Skill gdm257/cc-plugins/plugins/agent-skills/skills/opencode-plugin-development
This skill should be used when the user asks to "create an OpenCode plugin", "develop an OpenCode plugin", "write an OpenCode plugin", "build an OpenCode plugin", "make an OpenCode plugin", "add a hook to OpenCode", "subscribe to OpenCode events", "create a custom tool for OpenCode", or needs guidance on OpenCode plugin architecture, events, custom tools, TypeScript support, or dependencies management.From its SKILL.md
npx -y skills add gdm257/cc-plugins --skill opencode-plugin-developmentAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 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.
SKILL.md
19.4 KB, ~3.6k tokens by cl100k_base, as published. Nobody here has run it
OpenCode Plugin Development Guide
Create plugins to extend OpenCode's functionality by hooking into events, adding custom tools, and modifying behavior.
Overview
OpenCode plugins are JavaScript/TypeScript modules that export a plugin function. The function receives a context object and returns a hooks object defining event hooks and custom tools.
Plugin Types
OpenCode supports two plugin installation methods:
Config Plugin Field (Recommended)
Define plugins in your opencode.json plugin array:
{
"plugin": ["[email protected]", "@scope/enterprise-plugin"]
}
Characteristics:
- Plugins are npm packages installed via Bun
- Dependencies declared in
package.jsonare automatically installed - Published to npm registry for distribution
- Version management via npm versioning
- Suitable for: distributable plugins, plugins with external dependencies
Local Plugin Directory
Place plugin files directly in .opencode/plugins/:
.opencode/plugins/
└── my-plugin.ts
Characteristics:
- Local files loaded via
file://URLs - No dependency installation - all dependencies must be pre-installed
- Quick iteration for development
- Suitable for: private plugins, prototypes, plugins without external dependencies
Comparison:
| Feature | Config plugin Field | Local .opencode/plugins/ |
|---|---|---|
| Dependency Installation | ✅ Automatic via Bun | ❌ Manual |
| Distribution | npm package | Local files |
| Version Control | npm versioning | File-based |
| Use Case | Production plugins | Development/Private |
Recommendation: Use the config plugin field for most plugins.
Dependencies
Local plugins in .opencode/plugins/ cannot import external npm packages.
Plugins run within the OpenCode server process, which means:
-
Available imports:
- Node.js/Bun built-in modules (
fs,path,http, etc.) @opencode-ai/pluginSDK (official API)- Any packages already installed in the OpenCode server environment
- Node.js/Bun built-in modules (
-
Cannot import:
- Arbitrary npm packages not pre-installed in the server
- Dependencies from your project's
node_modules
Example - Only use built-ins and official SDK:
import type { Plugin } from "@opencode-ai/plugin"
import { readFileSync } from "fs" // ✅ Built-in module
import { client } from "./local" // ✅ Relative import
export const MyPlugin: Plugin = async () => {
return {
// Return hooks
}
}
To use npm dependencies, publish as an npm package:
- Create an npm package with your plugin
- Publish to npm registry
- Reference it in your config:
{
"plugin": ["[email protected]"]
}
Dependencies declared in the package's package.json will be automatically installed via Bun.
TypeScript Support
Import types from @opencode-ai/plugin:
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async (ctx) => {
// ctx is fully typed
const { project, client, $, directory, worktree } = ctx
return {
// Return hooks
}
}
Context Properties
The plugin function receives:
| Property | Type | Description |
|---|---|---|
project | object | Current project information |
client | SDK client | Interact with the AI assistant |
$ | Bun shell | Execute shell commands |
directory | string | Current working directory |
worktree | string | Git worktree path |
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
return {
// Return hooks
}
}
Hooks Reference
Plugins extend OpenCode through hooks - callback functions that run at specific times. A plugin provides hooks to react to events, register custom tools, or modify global configuration.
Complete plugin structure with all available hooks:
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async (input) => {
return {
// === Event Hooks (called each time the event occurs) ===
// Subscribe to all events (session.*, file.*, message.*, etc.)
event: async ({ event }) => {
switch (event.type) {
case "session.idle":
break
case "file.edited":
break
// ... other events
}
},
// Specific event hooks with custom input/output
"chat.message": async ({ sessionID, agent, model, messageID, variant }, { message, parts }) => {},
"chat.params": async ({ sessionID, agent, model, provider, message }, { temperature, topP, topK, options }) => {},
"chat.headers": async ({ sessionID, agent, model, provider, message }, { headers }) => {},
"permission.ask": async (input, { status }) => {},
"command.execute.before": async ({ command, sessionID, arguments }, { parts }) => {},
"tool.execute.before": async ({ tool, sessionID, callID }, { args }) => {},
"tool.execute.after": async ({ tool, sessionID, callID }, { title, output, metadata }) => {},
// Experimental event hooks
"experimental.chat.messages.transform": async (_, { messages }) => {},
"experimental.chat.system.transform": async ({ sessionID, model }, { system }) => {},
"experimental.session.compacting": async ({ sessionID }, { context, prompt }) => {},
"experimental.text.complete": async ({ sessionID, messageID, partID }, { text }) => {},
// === Registration Hooks (called once at initialization) ===
tool: {
[name: string]: {
description: string,
args: { [key: string]: z.ZodType },
execute: async (args, context) => string,
},
},
auth: {
provider: string,
loader: async (getAuth, provider) => Record<string, any>,
methods: Array<{ type: "oauth" | "api", label: string, authorize: async (inputs?) => {...} }>,
},
config: async (config) => {},
}
}
Hooks Summary
| Hook | Description | When |
|---|---|---|
| Event Hooks (per-event) | ||
event | Catch all events (session., file., message.*, etc.) via event.type | each |
chat.message | React to new messages | each |
chat.params | Modify LLM params | each |
chat.headers | Modify LLM headers | each |
permission.ask | Handle permissions | each |
command.execute.before | Before command | each |
tool.execute.before | Before tool | each |
tool.execute.after | After tool | each |
| Experimental Event Hooks (per-event) | ||
experimental.chat.messages.transform | Transform messages | each |
experimental.chat.system.transform | Transform system | each |
experimental.session.compacting | Session compaction | each |
experimental.text.complete | Text completion | each |
| Registration Hooks (once at init) | ||
tool | Register tools | init |
auth | Register auth | init |
config | Modify config | init |
All events available via event hook:
| Category | Events |
|---|---|
| Session | session.created, session.compacted, session.deleted, session.diff, session.error, session.idle, session.status, session.updated |
| File | file.edited, file.watcher.updated |
| Message | message.updated, message.removed, message.part.updated, message.part.removed |
| Command | command.executed |
| Permission | permission.asked, permission.replied |
| LSP | lsp.client.diagnostics, lsp.updated |
| Todo | todo.updated |
| Shell | shell.env |
| Server | server.connected, server.instance.disposed |
| Installation | installation.updated |
| TUI | tui.prompt.append, tui.command.execute, tui.toast.show, tui.session.select |
Custom Tools
Add custom tools using the tool helper:
import { tool } from "@opencode-ai/plugin"
export const CustomToolsPlugin: Plugin = async () => {
return {
tool: {
greet: tool({
description: "Greet a user by name",
args: {
name: tool.schema.string(),
},
async execute(args) {
return `Hello, ${args.name}!`
},
}),
calculate: tool({
description: "Perform basic calculations",
args: {
a: tool.schema.number(),
b: tool.schema.number(),
operation: tool.schema.enum(["add", "subtract", "multiply", "divide"]),
},
async execute(args) {
switch (args.operation) {
case "add":
return args.a + args.b
case "subtract":
return args.a - args.b
case "multiply":
return args.a * args.b
case "divide":
return args.a / args.b
}
},
}),
},
}
}
Slash Commands
Register slash commands and handle their execution using the config callback and command.execute.before event.
Command Registration
Use the config callback to register slash commands in OpenCode's configuration:
import type { Plugin } from "@opencode-ai/plugin"
export const SlashCommandPlugin: Plugin = async () => {
return {
config: async (cfg) => {
cfg.command ??= {}
cfg.command["myplugin"] = {
template: "",
description: "Execute my plugin commands",
}
},
}
}
Result:
- Command
/mypluginappears in autocomplete when user types/ - Description shows in command help UI
Command Handling
Intercept and process command execution via command.execute.before:
export const SlashCommandPlugin: Plugin = async () => {
return {
"command.execute.before": async (input, _output) => {
if (input.command === "myplugin") {
const args = (input.arguments || "").trim().split(/\s+/).filter(Boolean)
const subcommand = args[0]?.toLowerCase() || ""
if (subcommand === "status") {
// Handle /myplugin status
console.log("Status command executed")
} else if (subcommand === "config") {
// Handle /myplugin config
console.log("Config command executed")
} else {
// Handle unknown subcommand - show help
console.log("Available: status, config")
Config Modifications
The config callback receives the full OpenCode configuration object, allowing you to modify settings beyond just commands:
import type { Plugin } from "@opencode-ai/plugin"
export const ConfigPlugin: Plugin = async () => {
return {
config: async (cfg) => {
// Model configuration
cfg.model = "anthropic/claude-sonnet-4-20250506"
cfg.small_model = "anthropic/haiku-4-20250506"
cfg.default_agent = "build"
// Agent configuration
cfg.agent ??= {}
cfg.agent.build ??= {}
cfg.agent.build.model = "anthropic/claude-sonnet-4-20250506"
cfg.agent.build.temperature = 0.5
cfg.agent.build.prompt = "You are a careful developer."
// Experimental features
cfg.experimental ??= {}
cfg.experimental.batch_tool = true
cfg.experimental.primary_tools = ["read", "bash", "edit"]
cfg.experimental.continue_loop_on_deny = true
// Provider configuration
cfg.provider ??= {}
cfg.provider.anthropic ??= {}
cfg.provider.anthropic.options ??= {}
cfg.provider.anthropic.options.timeout = 60000
// Permission configuration
cfg.permission ??= {}
cfg.permission.read = "allow"
cfg.permission.edit = "ask"
// MCP servers
cfg.mcp ??= {}
cfg.mcp["pokemon-server"] = {
type: "local",
command: ["uv", "--directory", "/path/to/server", "run", "server"],
enabled: true,
}
// Server settings
cfg.server ??= {}
cfg.server.port = 4096
cfg.server.hostname = "0.0.0.0"
cfg.server.mdns = true
// UI settings
cfg.username = "my-user"
cfg.share = "manual"
// Compaction settings
cfg.compaction ??= {}
cfg.compaction.auto = true
cfg.compaction.prune = true
// Workspace backend
cfg.workspaceBackend = "git"
},
}
}
Available configuration fields:
| Field | Type | Description |
|---|---|---|
model | string | Default model (format: provider/model) |
small_model | string | Small model for auxiliary tasks |
default_agent | string | Default agent name |
agent | object | Agent configurations (build, plan, general, explore, etc.) |
provider | object | Custom provider and model overrides |
command | object | Slash commands registration |
mcp | object | MCP server configurations |
experimental | object | Experimental feature flags |
permission | object | Tool permission policies |
server | object | Server settings (port, hostname, mdns, cors) |
username | string | Display name |
share | "manual" | "auto" | "disabled" | Sharing behavior |
compaction | object | Session compaction settings |
workspaceBackend | "git" | Workspace backend |
Auth Hook
Register custom authentication providers:
import type { Plugin } from "@opencode-ai/plugin"
export const AuthPlugin: Plugin = async () => {
return {
auth: {
provider: "my-provider",
methods: [
{
type: "oauth",
label: "Sign in with MyProvider",
prompts: [{ type: "text", key: "tenant", message: "Enter tenant ID" }],
authorize: async (inputs) => {
const response = await fetch("https://my-provider.com/oauth/token", {
method: "POST",
body: JSON.stringify({
client_id: inputs.clientId,
client_secret: inputs.clientSecret,
tenant: inputs.tenant,
}),
})
const data = await response.json()
return {
type: "success",
provider: "my-provider",
refresh: data.refresh_token,
access: data.access_token,
expires: data.expires_in,
}
},
},
],
},
}
}
Logging
Use structured logging via client.app.log():
await client.app.log({
body: {
service: "my-plugin",
level: "debug" | "info" | "warn" | "error",
message: "Log message",
extra: {
// Additional context
foo: "bar",
},
},
})
Additional Resources
Detailed Reference
references/events.md- Complete event reference with full type signatures
Example Plugins
Complete, runnable plugin examples in examples/:
| File | Demonstrates |
|---|---|
notification.ts | Session events (session.created, session.idle, etc.) |
env-protection.ts | tool.execute.before, file.edited hooks |
inject-env.ts | shell.env hook, multiple plugin patterns |
compaction.ts | experimental.session.compacting variations |
custom-tools.ts | Custom tool registration with tool helper |
What ships with it: 6 files
25.0 KB alongside SKILL.md, 5 of them executable
examples/
- compaction.tsruns3.2 KB
- custom-tools.tsruns3.4 KB
- env-protection.tsruns1.9 KB
- inject-env.tsruns2.7 KB
- notification.tsruns1.6 KB
references/
- events.md12.2 KB