opencode-plugin-dev
Guide for developing OpenCode plugins using TypeScript - covers plugin structure, hooks, tools, authentication, events, testing, and best practices
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Guide for developing OpenCode plugins using TypeScript - covers plugin structure, hooks, tools, authentication, events, testing, and best practices
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
Guide the agent through iterative PR/MR refinement to ensure all CI checks, tests, linting, and validation passes before considering the work complete. Never declare success until all automated checks pass.
Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
Use when completing tasks, implementing major features, or before merging to verify work meets requirements
Use when executing implementation plans with independent tasks in the current session
| name | opencode-plugin-dev |
| description | Guide for developing OpenCode plugins using TypeScript - covers plugin structure, hooks, tools, authentication, events, testing, and best practices |
OpenCode plugins extend the functionality of the OpenCode AI assistant by allowing you to add custom tools, authentication providers, event handlers, and hooks into the core system. This skill guides you through creating, configuring, and distributing plugins.
Use this skill when:
A plugin is a TypeScript module that exports a function conforming to the Plugin type:
import { Plugin, tool } from '@opencode-ai/plugin'
export const MyPlugin: Plugin = async (ctx) => {
return {
tool: {
myTool: tool({
description: 'Custom tool',
args: { input: tool.schema.string() },
execute: async (args) => `Result: ${args.input}`,
}),
},
auth: {
provider: 'myservice',
methods: [{ type: 'api', label: 'API Key' }],
},
event: async ({ event }) => console.log(event.type),
config: async (config) => (config.myPlugin = { enabled: true }),
'chat.message': async ({}, { message }) => console.log(message.content),
'chat.params': async (
{ model, provider, message },
{ temperature, topP, options },
) => {
temperature = 0.7
options.custom = 'value'
},
'permission.ask': async (perm, out) => (out.status = 'allow'),
'tool.execute.before': async ({ tool }, { args }) => (args.modified = true),
'tool.execute.after': async ({ tool }, { title, output, metadata }) => {
console.log(`Tool ${tool} completed:`, output)
},
}
}
// Plugin Context (ctx) API Overview
ctx.client // Opencode SDK client (localhost:4096)
ctx.project.id // Project identifier (git hash or "global")
ctx.project.worktree // Git worktree root directory
ctx.project.vcs // Version control system ("git" or undefined)
ctx.directory // Current working directory
ctx.worktree // Git worktree root (alias for ctx.project.worktree)
ctx.$`command` // Bun shell for executing commands
ctx.$`git status`.text() // Shell command with output methods
mkdir my-opencode-plugin
cd my-opencode-plugin
bun init
{
"dependencies": {
"@opencode-ai/plugin": "latest",
"@opencode-ai/sdk": "latest",
"zod": "latest"
},
"devDependencies": {
"@types/node": "latest",
"typescript": "latest"
}
}
{
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"module": "preserve",
"declaration": true,
"moduleResolution": "bundler"
},
"include": ["src"]
}
// src/index.ts
import { Plugin, tool } from '@opencode-ai/plugin'
export const MyPlugin: Plugin = async (ctx) => {
return {
tool: {
hello: tool({
description: 'Say hello',
args: {
name: tool.schema.string().describe('Name to greet'),
},
async execute({ name }) {
return `Hello, ${name}!`
},
}),
},
}
}
Add custom tools that the AI can use:
import { tool } from '@opencode-ai/plugin'
export const MyPlugin: Plugin = async (ctx) => {
return {
tool: {
mytool: tool({
description: 'This is a custom tool',
args: {
foo: tool.schema.string().describe('foo parameter'),
count: tool.schema.number().optional().describe('optional count'),
},
async execute(args, context) {
// context includes: sessionID, messageID, agent, abort
return `Hello ${args.foo}! Count: ${args.count || 1}`
},
}),
},
}
}
Add custom authentication methods:
export const MyPlugin: Plugin = async (ctx) => {
return {
auth: {
provider: 'myservice',
loader: async (auth, provider) => {
// Load authentication configuration
return { apiKey: 'loaded-key' }
},
methods: [
{
type: 'oauth',
label: 'Connect MyService',
async authorize() {
return {
url: 'https://myservice.com/oauth/authorize',
instructions: 'Authorize Opencode to access MyService',
method: 'code',
async callback(code) {
// Handle OAuth callback
return {
type: 'success',
access: 'access-token',
refresh: 'refresh-token',
expires: Date.now() + 3600000,
}
},
}
},
},
],
},
}
}
Respond to system events:
export const MyPlugin: Plugin = async (ctx) => {
return {
event: async ({ event }) => {
console.log('Event received:', event.type)
},
}
}
Intercept and modify chat messages:
export const MyPlugin: Plugin = async (ctx) => {
return {
'chat.message': async ({}, { message, parts }) => {
// Modify message before sending to LLM
console.log('Message:', message.content)
},
}
}
Modify LLM parameters:
export const MyPlugin: Plugin = async (ctx) => {
return {
'chat.params': async (
{ model, provider, message },
{ temperature, topP, options },
) => {
// Adjust parameters based on context
temperature = 0.7
options.customParam = 'value'
},
}
}
Control permission requests:
export const MyPlugin: Plugin = async (ctx) => {
return {
'permission.ask': async (permission, output) => {
// Auto-allow certain permission types
if (permission.type === 'read_file') {
output.status = 'allow'
}
},
}
}
Intercept tool execution:
export const MyPlugin: Plugin = async (ctx) => {
return {
'tool.execute.before': async ({ tool, sessionID, callID }, { args }) => {
// Modify arguments before execution
if (tool === 'mytool') {
args.modified = true
}
},
'tool.execute.after': async (
{ tool, sessionID, callID },
{ title, output, metadata },
) => {
// Process tool results
console.log(`Tool ${tool} executed:`, output)
},
}
}
Modify Opencode configuration:
export const MyPlugin: Plugin = async (ctx) => {
return {
config: async (config) => {
// Add configuration options
config.myPlugin = { enabled: true }
},
}
}
Plugins have access to a Bun shell for executing commands:
export const MyPlugin: Plugin = async (ctx) => {
return {
tool: {
gitStatus: tool({
description: 'Get git status',
args: {},
async execute() {
const result = await ctx.$`git status --porcelain`
return result.text()
},
}),
},
}
}
Session Events:
session.created - New session createdsession.updated - Session updatedsession.deleted - Session deletedsession.error - Session error occurredsession.idle - Session became idleMessage Events:
message.updated - Message updatedmessage.removed - Message removedmessage.part.updated - Message part updatedmessage.part.removed - Message part removedFile Events:
file.edited - File was editedfile.watcher.updated - File watcher detected changes (add/change/unlink)Permission Events:
permission.updated - Permission updatedpermission.replied - Permission response receivedServer Events:
server.connected - Server connectedLSP Events:
lsp.updated - Language Server Protocol updatedlsp.diagnostics - LSP diagnostics availableCommand Events:
command.executed - Command executedTUI Events:
tui.prompt.append - Text appended to TUI prompttui.command.execute - Command executed in TUItui.toast.show - Toast shown in TUIOther Events:
installation.updated - Installation updatedide.installed - IDE extension installedAdd your plugin to opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["file:///path/to/your/plugin/dist/index.js"]
}
For published npm packages:
{
"plugin": ["my-opencode-plugin@1.0.0"]
}
{
"plugin": [
"plugin-one@latest",
"plugin-two@2.0.0",
"file:///path/to/local/plugin"
]
}
Use the prefix opencode- for plugin names:
opencode-my-serviceopencode-custom-toolsAlways handle errors gracefully:
export const MyPlugin: Plugin = async (ctx) => {
return {
tool: {
riskyTool: tool({
description: 'Tool that might fail',
args: {},
async execute() {
try {
const result = await ctx.$`some-command`
return result.text()
} catch (error) {
return `Error: ${error.message}`
}
},
}),
},
}
}
All plugin hooks are async - use proper async/await:
export const MyPlugin: Plugin = async (ctx) => {
return {
event: async ({ event }) => {
await processEvent(event)
},
}
}
Leverage TypeScript for type safety:
import { z } from 'zod'
export const MyPlugin: Plugin = async (ctx) => {
return {
tool: {
typedTool: tool({
description: 'Tool with typed arguments',
args: {
url: tool.schema.string().url().describe('Valid URL'),
count: tool.schema.number().min(1).max(100).describe('Count 1-100'),
},
async execute(args) {
// args are fully typed
return `Processing ${args.url} ${args.count} times`
},
}),
},
}
}
Clean up resources when needed:
export const MyPlugin: Plugin = async (ctx) => {
const cleanup = setupResource()
return {
event: async ({ event }) => {
if (event.type === 'shutdown') {
await cleanup()
}
},
}
}
// src/index.test.ts
import { describe, it, expect } from 'bun:test'
import { MyPlugin } from './index'
describe('MyPlugin', () => {
it('should register tools', async () => {
const mockCtx = createMockContext()
const hooks = await MyPlugin(mockCtx)
expect(hooks.tool).toBeDefined()
expect(hooks.tool.hello).toBeDefined()
})
})
Test with actual Opencode instance:
# Link local plugin for testing
bun link
cd /path/to/opencode/project
bun link my-opencode-plugin
# Add to opencode.json and test
Use console.log for debugging:
export const MyPlugin: Plugin = async (ctx) => {
console.log('Plugin loading with context:', ctx.project.name)
return {
tool: {
debugTool: tool({
description: 'Debug tool',
args: {},
async execute() {
console.log('Debug tool executed')
return 'Debug complete'
},
}),
},
}
}
Check plugin loading with:
opencode --verbose
// Send a message from a tool to the session
export const MyPlugin: Plugin = async (ctx) => {
return {
tool: {
sendPrompt: tool({
description: 'Send a prompt to the current session',
args: {
text: tool.schema.string().describe('Text to send'),
},
async execute(args, toolCtx) {
ctx.client.session.prompt({
path: { id: toolCtx.sessionID },
body: {
noReply: true,
parts: [{ type: 'text', text: args.text }],
},
})
return 'Prompt sent'
},
}),
},
}
}
export const FileSystemPlugin: Plugin = async (ctx) => {
return {
tool: {
listFiles: tool({
description: 'List files in directory',
args: {
path: tool.schema.string().describe('Directory path'),
},
async execute({ path }) {
const result = await ctx.$`ls -la ${path}`
return result.text()
},
}),
readFile: tool({
description: 'Read file contents',
args: {
path: tool.schema.string().describe('File path'),
},
async execute({ path }) {
const file = Bun.file(path)
return await file.text()
},
}),
},
}
}
export const APIPlugin: Plugin = async (ctx) => {
return {
tool: {
fetchAPI: tool({
description: 'Fetch data from API',
args: {
url: tool.schema.string().url().describe('API URL'),
method: tool.schema.enum(['GET', 'POST']).default('GET'),
},
async execute({ url, method }) {
const response = await fetch(url, { method })
return await response.text()
},
}),
},
}
}