creating-opencode-plugins

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Creating 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

事件分类

CategoryEventsUse Cases
command
command.executed
Track command history, notifications
file
file.edited
,
file.watcher.updated
File validation, auto-formatting
installation
installation.updated
Dependency tracking
lsp
lsp.client.diagnostics
,
lsp.updated
Custom error handling
message
message.*.updated/removed
Message filtering, logging
permission
permission.replied/updated
Permission policies
server
server.connected
Connection monitoring
session
session.created/deleted/error/idle/status/updated/compacted/diff
Session management
todo
todo.updated
Todo synchronization
tool
tool.execute.before/after
Tool interception, augmentation
tui
tui.prompt.append
,
tui.command.execute
,
tui.toast.show
UI customization
分类事件适用场景
command
command.executed
追踪命令历史、发送通知
file
file.edited
,
file.watcher.updated
文件验证、自动格式化
installation
installation.updated
依赖追踪
lsp
lsp.client.diagnostics
,
lsp.updated
自定义错误处理
message
message.*.updated/removed
消息过滤、日志记录
permission
permission.replied/updated
权限策略管理
server
server.connected
连接监控
session
session.created/deleted/error/idle/status/updated/compacted/diff
会话管理
todo
todo.updated
待办事项同步
tool
tool.execute.before/after
工具拦截、功能增强
tui
tui.prompt.append
,
tui.command.execute
,
tui.toast.show
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

安装位置

LocationPathScopeUse Case
Global
~/.config/opencode/plugin/
All projectsSecurity policies, global utilities
Project
.opencode/plugin/
Current projectProject-specific hooks, validators
位置路径作用范围适用场景
Global
~/.config/opencode/plugin/
所有项目安全策略、全局工具
Project
.opencode/plugin/
当前项目项目专属钩子、验证器

Common Mistakes

常见错误

MistakeWhy It FailsFix
Synchronous event handlerBlocks event loopUse
async
handlers
Missing error handlingPlugin crashes on errorWrap in try/catch
Heavy computation in handlerSlows down operationsDefer to background process
Mutating event data directlyCauses side effectsReturn override object
Not checking event typeHandles wrong eventsUse switch/case on
event.type
Forgetting context destructuringMissing key utilitiesDestructure
{ project, client, $, directory, worktree }
错误失败原因修复方案
同步事件处理器阻塞事件循环使用
async
处理器
缺少错误处理插件遇错崩溃用try/catch包裹逻辑
处理器中执行重计算拖慢操作速度延迟到后台进程执行
直接修改事件数据产生副作用返回override对象
未检查事件类型处理错误事件
event.type
使用switch/case判断
忘记解构context缺少关键工具函数解构
{ project, client, $, directory, worktree }

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 HookOpenCode EventDescription
PreToolUse
tool.execute.before
Run before tool execution, can block
PostToolUse
tool.execute.after
Run after tool execution
UserPromptSubmit
message.*
events
Process user prompts
SessionEnd
session.idle
Session completion
将Claude Code钩子行为迁移到OpenCode插件时,使用以下事件映射:
Claude钩子OpenCode事件描述
PreToolUse
tool.execute.before
工具执行前运行,可阻止执行
PostToolUse
tool.execute.after
工具执行后运行
UserPromptSubmit
message.*
事件
处理用户提示
SessionEnd
session.idle
会话结束时触发

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.json
重要提示:由于核心差异,OpenCode插件无法直接从Claude Code钩子转换:
  • 事件模型不同:Claude有4种钩子事件,OpenCode有32+种
  • 格式不同:Claude使用可执行脚本,OpenCode使用JS/TS模块
  • 执行上下文不同:上下文对象和返回值语义存在差异
将Claude钩子迁移到OpenCode插件时,需要使用OpenCode插件API重写逻辑。

Schema参考
packages/converters/schemas/opencode-plugin.schema.json