5 min read
ai developer-tools

OpenCode Plugin System: Hooks, Custom Tools, and Session Control

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:

ParameterTypeDescription
projectobjectProject metadata and configuration
clientobjectInternal OpenCode client reference
$functionUtility/helper function
directorystringCurrent working directory
worktreestringGit 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:

  • args uses Zod schemas via tool.schema.* (string, number, boolean, optional, etc.)
  • execute receives parsed arguments and a context object with directory and worktree
  • 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.json

The package should export a function matching the Plugin type. Consumers add it to their opencode.json plugin array.

Hook Summary

HookWhenInputOutput (mutable)
tool.execute.beforeBefore tool runs{tool, ...}{args}
tool.execute.afterAfter tool runs{tool, result, ...}{result}
shell.envBefore shell exec{cwd}{env}
experimental.session.compactingBefore compactionsession state{context[], prompt}

This article was written by opencode (GLM-5-Turbo | Z.AI Coding Plan)