What OpenCode Plugins Are
OpenCode plugins are async functions that run inside the OpenCode server process. They receive a context object and return an object containing hooks (event interceptors) and/or custom tools (new AI-callable functions). This is the extension mechanism for modifying how OpenCode behaves without forking the project.
Source: opencode.ai/docs/plugins, anomalyco/opencode on GitHub.
Plugin Shape
Every plugin exports a function matching the Plugin type from @opencode-ai/plugin:
import { type Plugin, tool } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async (ctx) => { return { // hooks and/or custom tools }}The ctx parameter provides:
| Parameter | Type | Description |
|---|---|---|
project | object | Project metadata and configuration |
client | object | Internal OpenCode client reference |
$ | function | Utility/helper function |
directory | string | Current working directory |
worktree | string | Git worktree path (if applicable) |
Available Hooks
Hooks are string-keyed functions in the returned object. Each receives (input, output) — output is mutable.
tool.execute.before
Intercepts tool calls before execution. Mutate output.args to change what gets executed. Throw an error to block the call entirely.
export const EnvProtection: Plugin = async () => ({ "tool.execute.before": async (input, output) => { if (input.tool === "read" && output.args.filePath.includes(".env")) { throw new Error("Do not read .env files") } }})tool.execute.after
Runs after a tool executes. Use for post-processing, logging, or transforming results.
shell.env
Injects environment variables into all shell executions (both AI tool calls and user terminals).
export const InjectEnv: Plugin = async () => ({ "shell.env": async (input, output) => { output.env.MY_API_KEY = "secret" output.env.PROJECT_ROOT = input.cwd }})experimental.session.compacting
Controls what context survives session compaction. Two modes:
Append context — push additional strings into output.context:
export const CompactionPlugin: Plugin = async (ctx) => ({ "experimental.session.compacting": async (input, output) => { output.context.push(`## Current State- Actively modifying: src/components/Header.vue- Decision: using Composition API with script setup`) }})Replace prompt entirely — override output.prompt with a custom compaction template:
export const CustomCompactionPlugin: Plugin = async (ctx) => ({ "experimental.session.compacting": async (input, output) => { output.prompt = `Summarize: current task, files being modified, blockers, and next steps.Format as a structured prompt for a new agent to resume work.` }})Custom Tools
Plugins can register new tools that the AI can call alongside built-in ones. Custom tools take precedence over built-in tools when there’s a name conflict.
import { type Plugin, tool } from "@opencode-ai/plugin"
export const CustomToolsPlugin: Plugin = async (ctx) => ({ tool: { deploy_check: tool({ description: "Check deployment status for a service", args: { service: tool.schema.string(), region: tool.schema.string().optional(), }, async execute(args, context) { const { directory } = context return `Checking ${args.service} in ${args.region ?? "us-east-1"}...` }, }), },})Key details:
argsuses Zod schemas viatool.schema.*(string, number, boolean, optional, etc.)executereceives parsed arguments and a context object withdirectoryandworktree- Return value is a string passed back to the AI
Real-World Examples
Shell escaping with external dependencies:
import { escape } from "shescape"
export const SafeBash: Plugin = async () => ({ "tool.execute.before": async (input, output) => { if (input.tool === "bash") { output.args.command = escape(output.args.command) } }})PTY management (opencode-pty) — spawns background terminal sessions as AI-callable tools (pty_spawn, pty_read, pty_write, pty_kill).
Registration
Add plugins to opencode.json in the project root:
{ "$schema": "https://opencode.ai/config.json", "plugin": ["opencode-pty", "opencode-helicone-session", "@my-org/custom-plugin"]}Supports both regular and scoped npm packages. OpenCode resolves and loads them at startup.
Publishing a Plugin
my-opencode-plugin/ package.json # name: "my-opencode-plugin" src/ index.ts # export const MyPlugin: Plugin = ... tsconfig.jsonThe package should export a function matching the Plugin type. Consumers add it to their opencode.json plugin array.
Hook Summary
| Hook | When | Input | Output (mutable) |
|---|---|---|---|
tool.execute.before | Before tool runs | {tool, ...} | {args} |
tool.execute.after | After tool runs | {tool, result, ...} | {result} |
shell.env | Before shell exec | {cwd} | {env} |
experimental.session.compacting | Before compaction | session state | {context[], prompt} |
This article was written by opencode (GLM-5-Turbo | Z.AI Coding Plan)

