Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521---title: Pluginsdescription: Write your own plugins to extend OpenCode.---
Plugins allow you to extend OpenCode by hooking into various events and customizing behavior. You can create plugins to add new features, integrate with external services, or modify OpenCode's default behavior.
For examples, check out the [plugins](/docs/ecosystem#plugins) created by the community.
---
## Use a plugin
There are two ways to load plugins.
---
### From local files
Place JavaScript or TypeScript files in the plugin directory.
- `.opencode/plugins/` - Project-level plugins- `~/.config/opencode/plugins/` - Global plugins
Files in these directories are automatically loaded at startup.
---
### From npm
Specify npm packages in your config file.
```json title="opencode.json"{ "$schema": "https://opencode.ai/config.json", "plugin": ["opencode-helicone-session", "opencode-wakatime", "@my-org/custom-plugin"]}```
Both regular and scoped npm packages are supported.
Browse available plugins in the [ecosystem](/docs/ecosystem#plugins).
---
### How plugins are installed
**npm plugins** are installed automatically using Bun at startup. Packages and their dependencies are cached in `~/.cache/opencode/node_modules/`.
**Local plugins** are loaded directly from the plugin directory. To use external packages, you must create a `package.json` within your config directory (see [Dependencies](#dependencies)), or publish the plugin to npm and [add it to your config](/docs/config#plugins).
---
### Load order
Plugins are loaded from all sources and all hooks run in sequence. The load order is:
1. Global config (`~/.config/opencode/opencode.json`)2. Project config (`opencode.json`)3. Global plugin directory (`~/.config/opencode/plugins/`)4. Project plugin directory (`.opencode/plugins/`)
Duplicate npm packages with the same name and version are loaded once. However, a local plugin and an npm plugin with similar names are both loaded separately.
---
## Create a plugin
A plugin is a **JavaScript/TypeScript module** that exports one or more pluginfunctions. Each function receives a context object and returns a hooks object.
---
### Dependencies
Local plugins and custom tools can use external npm packages. Add a `package.json` to your config directory with the dependencies you need.
```json title=".opencode/package.json"{ "dependencies": { "shescape": "^2.1.0" }}```
OpenCode runs `bun install` at startup to install these. Your plugins and tools can then import them.
```ts title=".opencode/plugins/my-plugin.ts"import { escape } from "shescape"
export const MyPlugin = async (ctx) => { return { "tool.execute.before": async (input, output) => { if (input.tool === "bash") { output.args.command = escape(output.args.command) } }, }}```
---
### Basic structure
```js title=".opencode/plugins/example.js"export const MyPlugin = async ({ project, client, $, directory, worktree }) => { console.log("Plugin initialized!")
return { // Hook implementations go here }}```
The plugin function receives:
- `project`: The current project information.- `directory`: The current working directory.- `worktree`: The git worktree path.- `client`: An opencode SDK client for interacting with the AI.- `$`: Bun's [shell API](https://bun.com/docs/runtime/shell) for executing commands.
---
### TypeScript support
For TypeScript plugins, you can import types from the plugin package:
```ts title="my-plugin.ts" {1}import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => { return { // Type-safe hook implementations }}```
---
## TUI exit epilogue
The epilogue prints one wordmark, global rows, and one envelope for every selected session. Global projections retain the existing current-route behavior and appear once before all session envelopes.
```tsreturn context.ui.epilogue.register(({ sessionID }) => { const session = context.data.session.get(sessionID) if (!session) return return { label: "Updated", value: { type: "relative-time", timestamp: session.time.updated }, }})```
Register a per-session projection when the row should repeat for each selected session:
```tsreturn context.ui.epilogue.registerSession(({ sessionID }) => { const cost = context.data.session.cost(sessionID) if (cost === 0) return return { label: "Cost", value: { type: "text", text: `$${cost.toFixed(2)}` } }})```
Both projection forms must be cheap and synchronous: read live cached data, return `undefined` when no row applies, and do not fetch or start other work. A global projection runs only while the current route has hydrated session metadata. A per-session projection runs only for loaded sessions selected from the current session and visible shared tabs.
### Publish retained rows
Use a retained handle for event-driven values that should not rerun a projection. Global retained rows do not require a current session.
```tsconst connection = context.ui.epilogue.retain("connection")const unsubscribe = context.data.on("server.connected", () => { connection.set({ label: "Server", value: { type: "text", text: "connected" } })})return () => { unsubscribe() connection.dispose()}```
Target one session explicitly for a scoped event value:
```tsconst route = context.ui.router.current()if (route.type === "session") { const outcome = context.ui.epilogue.retainSession(route.sessionID, "outcome") outcome.set({ label: "Outcome", value: { type: "text", text: "reviewed" } })}```
The session ID scopes the value but does not add that session to the epilogue inventory. `set(undefined)` omits the row; `dispose()` removes the entry and makes later writes no-ops. Keys are plugin-local and must be unique within the same global or session scope.
### Collect before exit
Register one batched collector when producing retained rows continuously would waste work. It receives the session IDs already selected for the epilogue and publishes through the same retained handles.
```tsreturn context.ui.epilogue.onCollect((event) => { const outputs = new Map( event.sessionIDs.map((sessionID) => [sessionID, context.ui.epilogue.retainSession(sessionID, "summary")]), ) event.waitUntil( readSummaries(event.sessionIDs, event.signal).then((summaries) => { summaries.forEach((text, sessionID) => { outputs.get(sessionID)?.set({ label: "Summary", value: { type: "text", text } }) }) }), )})```
Collection has one shared lifecycle:
- Normal exit, `SIGHUP`, `SIGINT`, and `SIGTERM` all run collectors.- All callbacks share one four-second window.- Pass asynchronous work to `waitUntil` during callback dispatch, or return one promise.- A throw or rejection affects only that collector.- At the deadline, OpenCode aborts `event.signal` and freezes whatever retained values have arrived.- Terminal restoration and plugin cleanup finish before OpenCode prints.- Unfinished and late work cannot change frozen output.
The deadline is cooperative: it cannot interrupt synchronous JavaScript or SQLite work that blocks the event loop.
### Transform session selection
Plugins can synchronously edit the configured rule array while active. Edits replay in plugin enable order and registration order whenever the config base or active plugin generation changes.
```tsreturn context.ui.epilogue.selection.transform((rules) => { rules.unshift({ type: "limit", count: 5, terminating: false })})```
Transforms may add, remove, reorder, or modify only `activity-within` and `limit` rules. The `terminating` field is the stage's explicit stopping grant. A transform never edits `cli.json`; disposal and hot reload rebuild from the configured base rather than applying inverse mutations. Throwing, asynchronous, or invalid transforms are discarded independently, and later transforms still run.
### Values, ordering, and lifecycle
Use `{ type: "text", text }` for already-formatted values. `relative-time` keeps the timestamp semantic so OpenCode can format every row against the same shutdown clock.
Global rows appear once before the session envelopes. Per-session rows follow plugin enable order and then registration order within each plugin. Duplicate plugin labels are allowed, while `Session` and `Continue` are reserved for OpenCode. The built-in `Last active` reporter uses cached `max(time.updated, time.idle)` even while a session is running; running status is a separate row rather than a fabricated current activity timestamp.
Each plugin may contribute through its first eight applicable registrations in the global lane and in each session lane. Labels are limited to 24 display columns and text values to 48; invalid or failing results omit only that registration and are logged without affecting other rows.
OpenCode tracks, validates, and copies projection results while the TUI is live. The exit sequence is:
1. Registered collectors may update retained rows.2. OpenCode freezes the rows.3. Ordinary plugin cleanup runs.4. OpenCode prints the immutable result.
Selection transforms and projections do not run during collection or teardown. No collector or retained setter can change the frozen output.
---
### Events
Plugins can subscribe to events as seen below in the Examples section. Here is a list of the different events available.
#### Command Events
- `command.executed`
#### File Events
- `file.edited`- `file.watcher.updated`
#### Installation Events
- `installation.updated`
#### LSP Events
- `lsp.client.diagnostics`- `lsp.updated`
#### Message Events
- `message.part.removed`- `message.part.updated`- `message.removed`- `message.updated`
#### Permission Events
- `permission.asked`- `permission.replied`
#### Server Events
- `server.connected`
#### Session Events
- `session.created`- `session.compacted`- `session.deleted`- `session.diff`- `session.error`- `session.idle`- `session.status`- `session.updated`
#### Shell Events
- `shell.env`
#### Tool Events
- `tool.execute.after`- `tool.execute.before`
#### TUI Events
- `tui.prompt.append`- `tui.command.execute`- `tui.toast.show`
---
## Examples
Here are some examples of plugins you can use to extend opencode.
---
### Send notifications
Send notifications when certain events occur:
```js title=".opencode/plugins/notification.js"export const NotificationPlugin = async ({ project, client, $, directory, worktree }) => { return { event: async ({ event }) => { // Send notification on session completion if (event.type === "session.idle") { await $`osascript -e 'display notification "Session completed!" with title "opencode"'` } }, }}```
We are using `osascript` to run AppleScript on macOS. Here we are using it to send notifications.
:::noteIf you’re using the OpenCode desktop app, it can send system notifications automatically when a response is ready or when a session errors.:::
---
### .env protection
Prevent opencode from reading `.env` files:
```javascript title=".opencode/plugins/env-protection.js"export const EnvProtection = 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") } }, }}```
---
### Inject environment variables
Inject environment variables into all shell execution (AI tools and user terminals):
```javascript title=".opencode/plugins/inject-env.js"export const InjectEnvPlugin = async () => { return { "shell.env": async (input, output) => { output.env.MY_API_KEY = "secret" output.env.PROJECT_ROOT = input.cwd }, }}```
---
### Custom tools
Plugins can also add custom tools to opencode:
```ts title=".opencode/plugins/custom-tools.ts"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} (worktree: ${worktree})` }, }), }, }}```
The `tool` helper creates a custom tool that opencode can call. It takes a Zod schema function and returns a tool definition with:
- `description`: What the tool does- `args`: Zod schema for the tool's arguments- `execute`: Function that runs when the tool is called
Your custom tools will be available to opencode alongside built-in tools.
:::noteIf a plugin tool uses the same name as a built-in tool, the plugin tool takes precedence.:::
---
### Logging
Use `client.app.log()` instead of `console.log` for structured logging:
```ts title=".opencode/plugins/my-plugin.ts"export const MyPlugin = async ({ client }) => { await client.app.log({ body: { service: "my-plugin", level: "info", message: "Plugin initialized", extra: { foo: "bar" }, }, })}```
Levels: `debug`, `info`, `warn`, `error`. See [SDK documentation](https://opencode.ai/docs/sdk) for details.
---
### Compaction hooks
Customize the context included when a session is compacted:
```ts title=".opencode/plugins/compaction.ts"import type { Plugin } from "@opencode-ai/plugin"
export const CompactionPlugin: Plugin = async (ctx) => { return { "experimental.session.compacting": async (input, output) => { // Inject additional context into the compaction prompt output.context.push(`## Custom Context
Include any state that should persist across compaction:- Current task status- Important decisions made- Files being actively worked on`) }, }}```
The `experimental.session.compacting` hook fires before the LLM generates a continuation summary. Use it to inject domain-specific context that the default compaction prompt would miss.
You can also replace the compaction prompt entirely by setting `output.prompt`:
```ts title=".opencode/plugins/custom-compaction.ts"import type { Plugin } from "@opencode-ai/plugin"
export const CustomCompactionPlugin: Plugin = async (ctx) => { return { "experimental.session.compacting": async (input, output) => { // Replace the entire compaction prompt output.prompt = `You are generating a continuation prompt for a multi-agent swarm session.
Summarize:1. The current task and its status2. Which files are being modified and by whom3. Any blockers or dependencies between agents4. The next steps to complete the work
Format as a structured prompt that a new agent can use to resume work.` }, }}```
When `output.prompt` is set, it completely replaces the default compaction prompt. The `output.context` array is ignored in this case.
---
## Session reads
Plugins can create sessions, enumerate them, or read retained projected history. `children` is a plugin-only paginated view of direct child sessions; it includes forks.
```tsconst created = await ctx.session.create({ title: "Review" })const session = await ctx.session.get({ sessionID })const page = await ctx.session.list({ directory: ctx.location.directory, limit: 50 })const children = await ctx.session.children({ sessionID, limit: 50 })const history = await ctx.session.messages({ sessionID, order: "asc", limit: 50 })const context = await ctx.session.context({ sessionID })const pending = await ctx.session.inbox.list({ sessionID })const active = await ctx.session.active()```
`list`, `children`, and `messages` return `cursor.previous` and `cursor.next` values for pagination. `messages` reads retained history across completed compactions, while `context` returns only the messages in the active model context after the last completed compaction. `inbox.list` returns durable work that has not yet been delivered. `active` reports sessions currently running in this OpenCode process. The plugin inbox surface is read-only.