🎯

creating-opencode-plugins

🎯Skill

from pr-pm/prpm

VibeIndex|
What it does

Develops event-driven OpenCode plugins to intercept and customize interactions across command, file, LSP, permission, session, and tool events.

creating-opencode-plugins

Installation

Install skill:
npx skills add https://github.com/pr-pm/prpm --skill creating-opencode-plugins
2
AddedJan 25, 2026

Skill Details

SKILL.md

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 implementation patterns for JavaScript/TypeScript event-driven modules

Overview

# 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

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

}

}

};

};

```

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)

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

}

}

}

};

};

```

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;

}

}

}

};

};

```

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

}

}

};

};

```

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

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

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

}

});

```

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

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

}

}

};

};

```

Plugin Composition

Combine multiple plugins using [opencode-plugin-compose](https://github.com/ericc-ch/opencode-plugins):

```javascript

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/

More from this repository10

🎯
beanstalk-deploy🎯Skill

Deploys and manages AWS Elastic Beanstalk applications with robust infrastructure health checks, error handling, and GitHub Actions integration.

🎯
human-writing🎯Skill

Writes content with authentic human voice, avoiding AI-generated patterns and corporate jargon for genuine, conversational communication.

🎯
self-improving🎯Skill

I'll search for relevant PRPM packages to help with Pulumi and Beanstalk infrastructure. Suggested Packages: 1. @prpm/pulumi-beanstalk (Official, 5,000+ downloads) 2. pulumi-aws-extensions (Commun...

🎯
creating-opencode-agents🎯Skill

Generates OpenCode agent configurations with markdown, YAML frontmatter, and precise tool/permission settings for specialized AI assistants.

🎯
prpm-development🎯Skill

Develops a universal package manager for AI prompts, enabling seamless discovery, installation, sharing, and management of development artifacts across AI platforms.

🎯
thoroughness🎯Skill

thoroughness skill from pr-pm/prpm

🎯
creating-continue-packages🎯Skill

Generates Continue rules with required naming, context-aware matching using globs/regex, and markdown formatting for package configurations.

🎯
creating-zed-extensions🎯Skill

I'm ready to assist you!".to_string(), sections: vec![], }) } _ => Err(format!("Unknown command: {}", command.name)), } } } ...

🎯
prpm-json-best-practices🎯Skill

Guides developers in creating high-quality, well-structured PRPM package manifests with comprehensive best practices and multi-package management strategies.

🎯
elastic-beanstalk-deployment🎯Skill

Deploys and troubleshoots Node.js applications on AWS Elastic Beanstalk by managing dependency installation, handling monorepo complexities, and ensuring reliable deployments.