creating-opencode-plugins

Creating OpenCode Plugins

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "creating-opencode-plugins" with this command: npx skills add pr-pm/prpm/pr-pm-prpm-creating-opencode-plugins

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/

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

creating-opencode-agents

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript-type-safety

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

github-actions-testing

No summary provided by upstream source.

Repository SourceNeeds Review