| name | opencode-plugin-dev |
| description | Guide for developing OpenCode plugins using TypeScript - covers plugin structure, hooks, tools, authentication, events, testing, and best practices |
Overview
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.
When to Use This Skill
Use this skill when:
- Creating a new OpenCode plugin
- Adding custom tools for the AI to use
- Implementing authentication providers
- Responding to system events
- Modifying chat parameters or permissions
- Testing or debugging plugins
Plugin Structure
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
ctx.client
ctx.project.id
ctx.project.worktree
ctx.project.vcs
ctx.directory
ctx.worktree
ctx.$`command`
ctx.$`git status`.text()
Project Setup
1. Create Plugin Package
mkdir my-opencode-plugin
cd my-opencode-plugin
bun init
2. Install Dependencies
{
"dependencies": {
"@opencode-ai/plugin": "latest",
"@opencode-ai/sdk": "latest",
"zod": "latest"
},
"devDependencies": {
"@types/node": "latest",
"typescript": "latest"
}
}
3. Configure TypeScript
{
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"module": "preserve",
"declaration": true,
"moduleResolution": "bundler"
},
"include": ["src"]
}
4. Create Plugin Source
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}!`
},
}),
},
}
}
Plugin Hooks Reference
Tool Definition
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) {
return `Hello ${args.foo}! Count: ${args.count || 1}`
},
}),
},
}
}
Authentication Providers
Add custom authentication methods:
export const MyPlugin: Plugin = async (ctx) => {
return {
auth: {
provider: 'myservice',
loader: async (auth, provider) => {
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) {
return {
type: 'success',
access: 'access-token',
refresh: 'refresh-token',
expires: Date.now() + 3600000,
}
},
}
},
},
],
},
}
}
Event Handlers
Respond to system events:
export const MyPlugin: Plugin = async (ctx) => {
return {
event: async ({ event }) => {
console.log('Event received:', event.type)
},
}
}
Chat Message Hooks
Intercept and modify chat messages:
export const MyPlugin: Plugin = async (ctx) => {
return {
'chat.message': async ({}, { message, parts }) => {
console.log('Message:', message.content)
},
}
}
Chat Parameter Modification
Modify LLM parameters:
export const MyPlugin: Plugin = async (ctx) => {
return {
'chat.params': async (
{ model, provider, message },
{ temperature, topP, options },
) => {
temperature = 0.7
options.customParam = 'value'
},
}
}
Permission Control
Control permission requests:
export const MyPlugin: Plugin = async (ctx) => {
return {
'permission.ask': async (permission, output) => {
if (permission.type === 'read_file') {
output.status = 'allow'
}
},
}
}
Tool Execution Hooks
Intercept tool execution:
export const MyPlugin: Plugin = async (ctx) => {
return {
'tool.execute.before': async ({ tool, sessionID, callID }, { args }) => {
if (tool === 'mytool') {
args.modified = true
}
},
'tool.execute.after': async (
{ tool, sessionID, callID },
{ title, output, metadata },
) => {
console.log(`Tool ${tool} executed:`, output)
},
}
}
Configuration Hook
Modify Opencode configuration:
export const MyPlugin: Plugin = async (ctx) => {
return {
config: async (config) => {
config.myPlugin = { enabled: true }
},
}
}
Shell Integration
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()
},
}),
},
}
}
Events Reference
Session Events:
session.created - New session created
session.updated - Session updated
session.deleted - Session deleted
session.error - Session error occurred
session.idle - Session became idle
Message Events:
message.updated - Message updated
message.removed - Message removed
message.part.updated - Message part updated
message.part.removed - Message part removed
File Events:
file.edited - File was edited
file.watcher.updated - File watcher detected changes (add/change/unlink)
Permission Events:
permission.updated - Permission updated
permission.replied - Permission response received
Server Events:
server.connected - Server connected
LSP Events:
lsp.updated - Language Server Protocol updated
lsp.diagnostics - LSP diagnostics available
Command Events:
command.executed - Command executed
TUI Events:
tui.prompt.append - Text appended to TUI prompt
tui.command.execute - Command executed in TUI
tui.toast.show - Toast shown in TUI
Other Events:
installation.updated - Installation updated
ide.installed - IDE extension installed
Plugin Configuration
Local Development
Add your plugin to opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["file:///path/to/your/plugin/dist/index.js"]
}
Published Plugins
For published npm packages:
{
"plugin": ["my-opencode-plugin@1.0.0"]
}
Multiple Plugins
{
"plugin": [
"plugin-one@latest",
"plugin-two@2.0.0",
"file:///path/to/local/plugin"
]
}
Building and Publishing
Use the prefix opencode- for plugin names:
opencode-my-service
opencode-custom-tools
Best Practices
1. Error Handling
Always 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}`
}
},
}),
},
}
}
2. Async Operations
All plugin hooks are async - use proper async/await:
export const MyPlugin: Plugin = async (ctx) => {
return {
event: async ({ event }) => {
await processEvent(event)
},
}
}
3. Type Safety
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) {
return `Processing ${args.url} ${args.count} times`
},
}),
},
}
}
4. Resource Management
Clean up resources when needed:
export const MyPlugin: Plugin = async (ctx) => {
const cleanup = setupResource()
return {
event: async ({ event }) => {
if (event.type === 'shutdown') {
await cleanup()
}
},
}
}
Testing Plugins
Unit Testing
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()
})
})
Integration Testing
Test with actual Opencode instance:
bun link
cd /path/to/opencode/project
bun link my-opencode-plugin
Debugging
Logging
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'
},
}),
},
}
}
Plugin Loading Issues
Check plugin loading with:
opencode --verbose
Recipe: Sending Session Prompt
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'
},
}),
},
}
}
Examples
File System Plugin
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()
},
}),
},
}
}
API Integration Plugin
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()
},
}),
},
}
}