OpenCode Plugin Creator
Overview
This skill helps you create custom OpenCode plugins that act as "hooks" to extend or modify OpenCode's behavior. Plugins can intercept events like file operations, tool executions, session lifecycle, and more.
When to Use
Use this skill when you need to:
- •Intercept and modify tool execution (e.g., prevent reading .env files)
- •React to session events (e.g., send notifications on completion)
- •Inject environment variables into shell commands
- •Add custom logging or monitoring
- •Create custom tools for OpenCode
- •Modify compaction behavior
- •Hook into file watcher events
- •Protect sensitive files or operations
Trigger phrases: "create plugin", "add hook", "intercept tool", "protect file from opencode", "custom opencode extension"
Plugin Loading Mechanisms
OpenCode loads plugins from two locations:
1. Local Files (Recommended for project-specific plugins)
- •Project-level:
.opencode/plugins/(JavaScript or TypeScript files) - •Global:
~/.config/opencode/plugins/ - •Files are automatically loaded at startup
2. npm Packages
- •Specified in
opencode.jsonconfig file - •Example:
{ "plugin": ["opencode-helicone-session"] } - •Auto-installed using Bun at startup
- •Cached in
~/.cache/opencode/node_modules/
Load order:
- •Global config (
~/.config/opencode/opencode.json) - •Project config (
opencode.json) - •Global plugin directory
- •Project plugin directory
Plugin Structure
Basic Template (JavaScript)
// .opencode/plugins/my-plugin.js
export const MyPlugin = async ({ project, client, $, directory, worktree }) => {
console.log("Plugin initialized!")
return {
// Hook implementations go here
}
}
TypeScript Template (Recommended)
// .opencode/plugins/my-plugin.ts
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
return {
// Type-safe hook implementations
}
}
Context Parameters
The plugin function receives:
- •
project: Current project information - •
directory: Current working directory - •
worktree: Git worktree path - •
client: OpenCode SDK client for AI interaction - •
$: Bun's shell API for executing commands
Available Events (Hooks)
Command Events
- •
command.executed: After a command is executed
File Events
- •
file.edited: After a file is edited - •
file.watcher.updated: When file watcher detects changes
Installation Events
- •
installation.updated: When dependencies are updated
LSP Events
- •
lsp.client.diagnostics: When LSP diagnostics are received - •
lsp.updated: When LSP is updated
Message Events
- •
message.part.removed: When message part is removed - •
message.part.updated: When message part is updated - •
message.removed: When message is removed - •
message.updated: When message is updated
Permission Events
- •
permission.asked: When permission is requested - •
permission.replied: When permission is replied to
Server Events
- •
server.connected: When server connects
Session Events
- •
session.created: When session is created - •
session.compacted: When session is compacted - •
session.deleted: When session is deleted - •
session.diff: When session diff occurs - •
session.error: When session error occurs - •
session.idle: When session becomes idle - •
session.status: When session status changes - •
session.updated: When session is updated
Todo Events
- •
todo.updated: When todo is updated
Shell Events
- •
shell.env: Before shell command execution (inject env vars)
Tool Events (Most Common)
- •
tool.execute.before: Before tool execution (intercept/modify) - •
tool.execute.after: After tool execution (post-process)
TUI Events
- •
tui.prompt.append: When prompt is appended in TUI - •
tui.command.execute: When command is executed in TUI - •
tui.toast.show: When toast notification is shown
Experimental Events
- •
experimental.session.compacting: Before session compaction (inject context)
Common Use Cases & Examples
1. File Protection (.env files)
import type { Plugin } from "@opencode-ai/plugin"
export const EnvProtection: Plugin = async ({ project, client, $, directory, worktree }) => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "read" && output.args.filePath.includes(".env")) {
throw new Error("Do not read .env files")
}
},
}
}
2. Notification on Session Completion
export const NotificationPlugin = async ({ project, client, $, directory, worktree }) => {
return {
event: async ({ event }) => {
if (event.type === "session.idle") {
await $`osascript -e 'display notification "Session completed!" with title "opencode"'`
}
},
}
}
3. Inject Environment Variables
export const InjectEnvPlugin = async () => {
return {
"shell.env": async (input, output) => {
output.env.MY_API_KEY = "secret"
output.env.PROJECT_ROOT = input.cwd
},
}
}
4. Custom Tool
import { type Plugin, tool } from "@opencode-ai/plugin"
export const CustomToolsPlugin: Plugin = async (ctx) => {
return {
tool: {
mytool: tool({
description: "This is a custom tool",
args: {
foo: tool.schema.string(),
},
async execute(args, context) {
const { directory, worktree } = context
return `Hello ${args.foo} from ${directory}`
},
}),
},
}
}
5. Structured Logging
export const LoggingPlugin = async ({ client }) => {
await client.app.log({
body: {
service: "my-plugin",
level: "info",
message: "Plugin initialized",
extra: { foo: "bar" },
},
})
}
6. Custom Compaction Hook
import type { Plugin } from "@opencode-ai/plugin"
export const CompactionPlugin: Plugin = async (ctx) => {
return {
"experimental.session.compacting": async (input, output) => {
// Inject additional context
output.context.push(`## Custom Context
Include state that should persist:
- Current task status
- Important decisions made
- Files being actively worked on`)
},
}
}
Using External Dependencies
If your plugin needs external npm packages, create a package.json in your config directory:
.opencode/package.json
{
"dependencies": {
"shescape": "^2.1.0",
"axios": "^1.6.0"
}
}
OpenCode runs bun install at startup. Import packages normally:
import { escape } from "shescape"
import axios from "axios"
export const MyPlugin = async (ctx) => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "bash") {
output.args.command = escape(output.args.command)
}
},
}
}
Plugin Creation Procedure
- •
Identify the Use Case
- •What event do you want to intercept?
- •What behavior do you want to modify?
- •Which hook(s) do you need?
- •
Choose Plugin Location
- •Project-specific:
.opencode/plugins/ - •Global:
~/.config/opencode/plugins/ - •npm package: For sharing/reuse
- •Project-specific:
- •
Create Plugin File
- •Use TypeScript for type safety (
.ts) - •Or JavaScript for simplicity (
.js) - •Export a named function (e.g.,
export const MyPlugin = ...)
- •Use TypeScript for type safety (
- •
Implement Hook(s)
- •Choose event(s) from the list above
- •Implement async handler function
- •Access
input(event data) andoutput(modifiable)
- •
Test Plugin
- •Restart OpenCode to load plugin
- •Verify hook is triggered
- •Check logs with
client.app.log()
- •
Handle Dependencies (if needed)
- •Create
.opencode/package.json - •Add required packages
- •OpenCode auto-installs on next startup
- •Create
Guidelines
Best Practices
- •Use TypeScript for type safety and better IDE support
- •Use structured logging (
client.app.log()) instead ofconsole.log - •Throw errors in
beforehooks to prevent tool execution - •Keep plugins focused on a single responsibility
- •Document hook behavior in comments
- •Test with different events to ensure correct trigger
Common Pitfalls
- •❌ Don't mutate
input— only modifyoutput - •❌ Don't block event loop with sync operations
- •❌ Don't rely on global state (plugins may reload)
- •❌ Don't forget to handle edge cases (null, undefined)
Debugging
- •Use
client.app.log()for structured logging - •Check OpenCode logs for plugin load errors
- •Verify plugin file is in correct directory
- •Ensure export name matches function name
File Naming Conventions
- •Use kebab-case:
my-plugin.ts(notMyPlugin.ts) - •Use descriptive names:
env-protection.ts(notep.ts) - •Avoid conflicts with existing plugins
Integration with multi-agent-ff15
When creating plugins for multi-agent-ff15:
- •Save to
.opencode/plugins/(project-level) - •Consider Comrade-specific behaviors (Noctis, Ignis, etc.)
- •Use plugins to enforce workflow rules
- •Protect sensitive files (queue/, config/)
- •Add logging for debugging multi-agent interactions
Example: Todo Continuation Hook
import type { Plugin } from "@opencode-ai/plugin"
export const TodoContinuationHook: Plugin = async ({ client }) => {
return {
"todo.updated": async (input, output) => {
const todos = input.todos
const hasInProgress = todos.some(t => t.status === "in_progress")
const hasCompleted = todos.some(t => t.status === "completed")
if (hasCompleted && !hasInProgress) {
await client.app.log({
body: {
service: "todo-continuation",
level: "info",
message: "All todos completed, suggesting continuation",
},
})
}
},
}
}
Resources
Summary
This skill provides the knowledge to create OpenCode plugins that act as hooks. Use it whenever you need to extend OpenCode's behavior by intercepting events, modifying tool execution, or adding custom functionality.