creating-opencode-plugins
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCreating OpenCode Plugins
创建OpenCode插件
Overview
概述
OpenCode plugins are JavaScript/TypeScript modules that hook into 25+ events across the OpenCode AI assistant lifecycle. Plugins export an async function receiving context (project, client, $, directory, worktree) and return an event handler.
OpenCode插件是可挂钩OpenCode AI助手生命周期中25+种事件的JavaScript/TypeScript模块。插件导出一个异步函数,接收context(project、client、$、directory、worktree)参数并返回事件处理器。
When to Use
适用场景
Create an OpenCode plugin when:
- Intercepting file operations (prevent sharing .env files)
- Monitoring command execution (notifications, logging)
- Processing LSP diagnostics (custom error handling)
- Managing permissions (auto-approve trusted operations)
- Reacting to session lifecycle (cleanup, initialization)
- Extending tool capabilities (custom tool registration)
- Enhancing TUI interactions (custom prompts, toasts)
Don't create for:
- Simple prompt instructions (use agents instead)
- One-time scripts (use bash tools)
- Static configuration (use settings files)
在以下场景创建OpenCode插件:
- 拦截文件操作(阻止分享.env文件)
- 监控命令执行(通知、日志记录)
- 处理LSP诊断信息(自定义错误处理)
- 管理权限(自动批准可信操作)
- 响应会话生命周期(清理、初始化)
- 扩展工具功能(自定义工具注册)
- 增强TUI交互(自定义提示、通知弹窗)
以下场景无需创建插件:
- 简单提示指令(使用agent替代)
- 一次性脚本(使用bash工具)
- 静态配置(使用设置文件)
Quick Reference
快速参考
Plugin Structure
插件结构
javascript
export const MyPlugin = async (context) => {
// context: { project, client, $, directory, worktree }
return {
event: async ({ event }) => {
// event: { type: 'event.name', data: {...} }
switch(event.type) {
case 'file.edited':
// Handle file edits
break;
case 'tool.execute.before':
// Pre-process tool execution
break;
}
}
};
};javascript
export const MyPlugin = async (context) => {
// context: { project, client, $, directory, worktree }
return {
event: async ({ event }) => {
// event: { type: 'event.name', data: {...} }
switch(event.type) {
case 'file.edited':
// 处理文件编辑
break;
case 'tool.execute.before':
// 预处理工具执行
break;
}
}
};
};Event Categories
事件分类
| Category | Events | Use Cases |
|---|---|---|
| command | | Track command history, notifications |
| file | | File validation, auto-formatting |
| installation | | Dependency tracking |
| lsp | | Custom error handling |
| message | | Message filtering, logging |
| permission | | Permission policies |
| server | | Connection monitoring |
| session | | Session management |
| todo | | Todo synchronization |
| tool | | Tool interception, augmentation |
| tui | | UI customization |
| 分类 | 事件 | 适用场景 |
|---|---|---|
| command | | 追踪命令历史、发送通知 |
| file | | 文件验证、自动格式化 |
| installation | | 依赖追踪 |
| lsp | | 自定义错误处理 |
| message | | 消息过滤、日志记录 |
| permission | | 权限策略管理 |
| server | | 连接监控 |
| session | | 会话管理 |
| todo | | 待办事项同步 |
| tool | | 工具拦截、功能增强 |
| tui | | UI自定义 |
Plugin Manifest (package.json or separate config)
插件清单(package.json或独立配置)
json
{
"name": "env-protection",
"description": "Prevents sharing .env files",
"version": "1.0.0",
"author": "Security Team",
"plugin": {
"file": "plugin.js",
"location": "global"
},
"hooks": {
"file": ["file.edited"],
"permission": ["permission.replied"]
}
}json
{
"name": "env-protection",
"description": "Prevents sharing .env files",
"version": "1.0.0",
"author": "Security Team",
"plugin": {
"file": "plugin.js",
"location": "global"
},
"hooks": {
"file": ["file.edited"],
"permission": ["permission.replied"]
}
}Implementation
实现示例
Complete Example: Environment File Protection
完整示例:环境文件保护
javascript
// .opencode/plugin/env-protection.js
export const EnvProtectionPlugin = async ({ project, client }) => {
const sensitivePatterns = [
/\.env$/,
/\.env\..+$/,
/credentials\.json$/,
/\.secret$/,
];
const isSensitiveFile = (filePath) => {
return sensitivePatterns.some(pattern => pattern.test(filePath));
};
return {
event: async ({ event }) => {
switch (event.type) {
case 'file.edited': {
const { path } = event.data;
if (isSensitiveFile(path)) {
console.warn(`⚠️ Sensitive file edited: ${path}`);
console.warn('This file should not be shared or committed.');
}
break;
}
case 'permission.replied': {
const { action, target, decision } = event.data;
// Block read/share operations on sensitive files
if ((action === 'read' || action === 'share') &&
isSensitiveFile(target) &&
decision === 'allow') {
console.error(`🚫 Blocked ${action} operation on sensitive file: ${target}`);
// Override permission decision
return {
override: true,
decision: 'deny',
reason: 'Sensitive file protection policy'
};
}
break;
}
}
}
};
};javascript
// .opencode/plugin/env-protection.js
export const EnvProtectionPlugin = async ({ project, client }) => {
const sensitivePatterns = [
/\.env$/,
/\.env\..+$/,
/credentials\.json$/,
/\.secret$/,
];
const isSensitiveFile = (filePath) => {
return sensitivePatterns.some(pattern => pattern.test(filePath));
};
return {
event: async ({ event }) => {
switch (event.type) {
case 'file.edited': {
const { path } = event.data;
if (isSensitiveFile(path)) {
console.warn(`⚠️ Sensitive file edited: ${path}`);
console.warn('This file should not be shared or committed.');
}
break;
}
case 'permission.replied': {
const { action, target, decision } = event.data;
// Block read/share operations on sensitive files
if ((action === 'read' || action === 'share') &&
isSensitiveFile(target) &&
decision === 'allow') {
console.error(`🚫 Blocked ${action} operation on sensitive file: ${target}`);
// Override permission decision
return {
override: true,
decision: 'deny',
reason: 'Sensitive file protection policy'
};
}
break;
}
}
}
};
};Example: Command Execution Notifications
示例:命令执行通知
javascript
// .opencode/plugin/notify.js
export const NotifyPlugin = async ({ project, $ }) => {
let commandStartTime = null;
return {
event: async ({ event }) => {
switch (event.type) {
case 'command.executed': {
const { command, args, status } = event.data;
commandStartTime = Date.now();
console.log(`▶️ Executing: ${command} ${args.join(' ')}`);
break;
}
case 'tool.execute.after': {
const { tool, duration, success } = event.data;
if (duration > 5000) {
// Notify for long-running operations
await $`osascript -e 'display notification "Completed in ${duration}ms" with title "${tool}"'`;
}
console.log(`✅ ${tool} completed in ${duration}ms`);
break;
}
}
}
};
};javascript
// .opencode/plugin/notify.js
export const NotifyPlugin = async ({ project, $ }) => {
let commandStartTime = null;
return {
event: async ({ event }) => {
switch (event.type) {
case 'command.executed': {
const { command, args, status } = event.data;
commandStartTime = Date.now();
console.log(`▶️ Executing: ${command} ${args.join(' ')}`);
break;
}
case 'tool.execute.after': {
const { tool, duration, success } = event.data;
if (duration > 5000) {
// Notify for long-running operations
await $`osascript -e 'display notification "Completed in ${duration}ms" with title "${tool}"'`;
}
console.log(`✅ ${tool} completed in ${duration}ms`);
break;
}
}
}
};
};Example: Custom Tool Registration
示例:自定义工具注册
javascript
// .opencode/plugin/custom-tools.js
export const CustomToolsPlugin = async ({ client }) => {
// Register custom tool on initialization
await client.registerTool({
name: 'lint',
description: 'Run linter on current file with auto-fix option',
parameters: {
type: 'object',
properties: {
fix: {
type: 'boolean',
description: 'Auto-fix issues'
}
}
},
handler: async ({ fix }) => {
const result = await $`eslint ${fix ? '--fix' : ''} .`;
return {
output: result.stdout,
errors: result.stderr
};
}
});
return {
event: async ({ event }) => {
// Monitor tool usage
if (event.type === 'tool.execute.before') {
console.log(`🔧 Tool: ${event.data.tool}`);
}
}
};
};javascript
// .opencode/plugin/custom-tools.js
export const CustomToolsPlugin = async ({ client }) => {
// Register custom tool on initialization
await client.registerTool({
name: 'lint',
description: 'Run linter on current file with auto-fix option',
parameters: {
type: 'object',
properties: {
fix: {
type: 'boolean',
description: 'Auto-fix issues'
}
}
},
handler: async ({ fix }) => {
const result = await $`eslint ${fix ? '--fix' : ''} .`;
return {
output: result.stdout,
errors: result.stderr
};
}
});
return {
event: async ({ event }) => {
// Monitor tool usage
if (event.type === 'tool.execute.before') {
console.log(`🔧 Tool: ${event.data.tool}`);
}
}
};
};Installation Locations
安装位置
| Location | Path | Scope | Use Case |
|---|---|---|---|
| Global | | All projects | Security policies, global utilities |
| Project | | Current project | Project-specific hooks, validators |
| 位置 | 路径 | 作用范围 | 适用场景 |
|---|---|---|---|
| Global | | 所有项目 | 安全策略、全局工具 |
| Project | | 当前项目 | 项目专属钩子、验证器 |
Common Mistakes
常见错误
| Mistake | Why It Fails | Fix |
|---|---|---|
| Synchronous event handler | Blocks event loop | Use |
| Missing error handling | Plugin crashes on error | Wrap in try/catch |
| Heavy computation in handler | Slows down operations | Defer to background process |
| Mutating event data directly | Causes side effects | Return override object |
| Not checking event type | Handles wrong events | Use switch/case on |
| Forgetting context destructuring | Missing key utilities | Destructure |
| 错误 | 失败原因 | 修复方案 |
|---|---|---|
| 同步事件处理器 | 阻塞事件循环 | 使用 |
| 缺少错误处理 | 插件遇错崩溃 | 用try/catch包裹逻辑 |
| 处理器中执行重计算 | 拖慢操作速度 | 延迟到后台进程执行 |
| 直接修改事件数据 | 产生副作用 | 返回override对象 |
| 未检查事件类型 | 处理错误事件 | 对 |
| 忘记解构context | 缺少关键工具函数 | 解构 |
Event Data Structures
事件数据结构
typescript
// File Events
interface FileEditedEvent {
type: 'file.edited';
data: {
path: string;
content: string;
timestamp: number;
};
}
// Tool Events
interface ToolExecuteBeforeEvent {
type: 'tool.execute.before';
data: {
tool: string;
args: Record<string, any>;
user: string;
};
}
interface ToolExecuteAfterEvent {
type: 'tool.execute.after';
data: {
tool: string;
duration: number;
success: boolean;
output?: any;
error?: string;
};
}
// Permission Events
interface PermissionRepliedEvent {
type: 'permission.replied';
data: {
action: 'read' | 'write' | 'execute' | 'share';
target: string;
decision: 'allow' | 'deny';
};
}typescript
// File Events
interface FileEditedEvent {
type: 'file.edited';
data: {
path: string;
content: string;
timestamp: number;
};
}
// Tool Events
interface ToolExecuteBeforeEvent {
type: 'tool.execute.before';
data: {
tool: string;
args: Record<string, any>;
user: string;
};
}
interface ToolExecuteAfterEvent {
type: 'tool.execute.after';
data: {
tool: string;
duration: number;
success: boolean;
output?: any;
error?: string;
};
}
// Permission Events
interface PermissionRepliedEvent {
type: 'permission.replied';
data: {
action: 'read' | 'write' | 'execute' | 'share';
target: string;
decision: 'allow' | 'deny';
};
}Testing Plugins
插件测试
javascript
// Test plugin locally before installation
import { EnvProtectionPlugin } from './env-protection.js';
const mockContext = {
project: { root: '/test/project' },
client: {},
$: async (cmd) => ({ stdout: '', stderr: '' }),
directory: '/test/project',
worktree: null
};
const plugin = await EnvProtectionPlugin(mockContext);
// Simulate event
await plugin.event({
event: {
type: 'file.edited',
data: { path: '.env', content: 'SECRET=123', timestamp: Date.now() }
}
});javascript
// Test plugin locally before installation
import { EnvProtectionPlugin } from './env-protection.js';
const mockContext = {
project: { root: '/test/project' },
client: {},
$: async (cmd) => ({ stdout: '', stderr: '' }),
directory: '/test/project',
worktree: null
};
const plugin = await EnvProtectionPlugin(mockContext);
// Simulate event
await plugin.event({
event: {
type: 'file.edited',
data: { path: '.env', content: 'SECRET=123', timestamp: Date.now() }
}
});Real-World Impact
实际应用价值
Security: Prevent accidental sharing of credentials (env-protection plugin blocks .env file reads)
Productivity: Auto-notify on long-running commands (notify plugin sends system notifications)
Quality: Auto-format files on save (file.edited hook runs prettier)
Monitoring: Track tool usage patterns (tool.execute hooks log analytics)
安全防护:防止意外泄露凭证(env-protection插件阻止.env文件读取)
效率提升:长时运行命令自动通知(notify插件发送系统通知)
质量保障:文件保存时自动格式化(file.edited钩子运行prettier)
监控分析:追踪工具使用模式(tool.execute钩子记录分析数据)
Claude Code Event Mapping
Claude Code事件映射
When porting Claude Code hook behavior to OpenCode plugins, use these event mappings:
| Claude Hook | OpenCode Event | Description |
|---|---|---|
| | Run before tool execution, can block |
| | Run after tool execution |
| | Process user prompts |
| | Session completion |
将Claude Code钩子行为迁移到OpenCode插件时,使用以下事件映射:
| Claude钩子 | OpenCode事件 | 描述 |
|---|---|---|
| | 工具执行前运行,可阻止执行 |
| | 工具执行后运行 |
| | 处理用户提示 |
| | 会话结束时触发 |
Example: Claude-like Hook Behavior
示例:类Claude钩子行为
javascript
export const CompatiblePlugin = async (context) => {
return {
// Equivalent to Claude's PreToolUse hook
'tool.execute.before': async (input, output) => {
if (shouldBlock(input)) {
throw new Error('Blocked by policy');
}
},
// Equivalent to Claude's PostToolUse hook
'tool.execute.after': async (result) => {
console.log(`Tool completed: ${result.tool}`);
},
// Equivalent to Claude's SessionEnd hook
event: async ({ event }) => {
if (event.type === 'session.idle') {
await cleanup();
}
}
};
};javascript
export const CompatiblePlugin = async (context) => {
return {
// 等价于Claude的PreToolUse钩子
'tool.execute.before': async (input, output) => {
if (shouldBlock(input)) {
throw new Error('Blocked by policy');
}
},
// 等价于Claude的PostToolUse钩子
'tool.execute.after': async (result) => {
console.log(`Tool completed: ${result.tool}`);
},
// 等价于Claude的SessionEnd钩子
event: async ({ event }) => {
if (event.type === 'session.idle') {
await cleanup();
}
}
};
};Plugin Composition
插件组合
Combine multiple plugins using opencode-plugin-compose:
javascript
import { compose } from "opencode-plugin-compose";
const composedPlugin = compose([
envProtectionPlugin,
notifyPlugin,
customToolsPlugin
]);
// Runs all hooks in sequence使用opencode-plugin-compose组合多个插件:
javascript
import { compose } from "opencode-plugin-compose";
const composedPlugin = compose([
envProtectionPlugin,
notifyPlugin,
customToolsPlugin
]);
// 按顺序运行所有钩子Non-Convertibility Note
不可直接转换说明
Important: OpenCode plugins cannot be directly converted from Claude Code hooks due to fundamental differences:
- Event models differ: Claude has 4 hook events, OpenCode has 32+
- Formats differ: Claude uses executable scripts, OpenCode uses JS/TS modules
- Execution context differs: Different context objects and return value semantics
When porting Claude hooks to OpenCode plugins, you'll need to rewrite the logic using the OpenCode plugin API.
Schema Reference:
packages/converters/schemas/opencode-plugin.schema.jsonDocumentation: https://opencode.ai/docs/plugins/
重要提示:由于核心差异,OpenCode插件无法直接从Claude Code钩子转换:
- 事件模型不同:Claude有4种钩子事件,OpenCode有32+种
- 格式不同:Claude使用可执行脚本,OpenCode使用JS/TS模块
- 执行上下文不同:上下文对象和返回值语义存在差异
将Claude钩子迁移到OpenCode插件时,需要使用OpenCode插件API重写逻辑。
Schema参考:
packages/converters/schemas/opencode-plugin.schema.json