Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    pr-pm

    creating-opencode-plugins

    pr-pm/creating-opencode-plugins
    Coding
    85
    5 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Use when creating OpenCode plugins that hook into command, file, LSP, message, permission, server, session, todo, tool, or TUI events - provides plugin structure, event API specifications, and...

    SKILL.md

    Creating OpenCode Plugins

    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.

    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)

    Quick Reference

    Plugin Structure

    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;
          }
        }
      };
    };
    

    Event Categories

    Category Events Use 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

    Plugin Manifest (package.json or separate config)

    {
      "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

    // .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

    // .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

    // .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 ~/.config/opencode/plugin/ All projects Security policies, global utilities
    Project .opencode/plugin/ Current project Project-specific hooks, validators

    Common Mistakes

    Mistake Why It Fails Fix
    Synchronous event handler Blocks event loop Use async handlers
    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 event.type
    Forgetting context destructuring Missing key utilities Destructure { project, client, $, directory, worktree }

    Event Data Structures

    // 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

    // 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)

    Claude Code Event Mapping

    When porting Claude Code hook behavior to OpenCode plugins, use these event mappings:

    Claude Hook OpenCode Event Description
    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

    Example: Claude-like Hook Behavior

    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();
          }
        }
      };
    };
    

    Plugin Composition

    Combine multiple plugins using opencode-plugin-compose:

    import { compose } from "opencode-plugin-compose";
    
    const composedPlugin = compose([
      envProtectionPlugin,
      notifyPlugin,
      customToolsPlugin
    ]);
    // Runs all hooks in sequence
    

    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

    Documentation: https://opencode.ai/docs/plugins/

    Repository
    pr-pm/prpm
    Files