docs(kilo-docs): add plugins documentation page

Document the plugin system for the Kilo CLI: installation paths, hooks
reference, custom tools, examples, and troubleshooting. Plugins were
previously only covered in upstream OpenCode docs.
This commit is contained in:
Josh Lambert
2026-04-20 23:37:47 -04:00
parent f0dd035437
commit 24385747f4
2 changed files with 492 additions and 0 deletions
+5
View File
@@ -33,6 +33,11 @@ export const AutomateNav: NavSection[] = [
href: "/automate/extending/shell-integration",
children: "Shell Integration",
},
{
href: "/automate/extending/plugins",
children: "Plugins",
platform: "new",
},
{
href: "/automate/extending/auto-launch",
children: "Auto-launch Configuration",
@@ -0,0 +1,487 @@
---
title: "Plugins"
description: "Extend the Kilo CLI with custom hooks, tools, auth providers, and more"
platform: new
---
# Plugins
Plugins extend the Kilo CLI by hooking into events, adding custom tools, registering auth or model providers, and customizing runtime behavior. They are TypeScript or JavaScript modules loaded at startup.
{% callout type="note" %}
Plugins run in the Kilo CLI. They are available everywhere the CLI runs — directly, through `kilo serve`, and inside the VS Code extension (which spawns `kilo serve` under the hood).
{% /callout %}
## What plugins can do
- **Add custom tools** the model can call (like `read`, `write`, `bash`).
- **Intercept tool calls** to mutate arguments, rewrite output, or block dangerous operations.
- **Subscribe to events** — sessions, messages, permissions, LSP diagnostics, file changes, etc.
- **Register auth providers** — OAuth or API-key flows for model providers.
- **Register model providers** — dynamic model catalogs.
- **Mutate chat parameters or headers** sent to the LLM.
- **Customize compaction** — inject or replace the prompt used when a session is compacted.
- **Inject shell environment variables** for commands executed by the agent or user.
---
## Use a plugin
There are three ways to load plugins.
### From a config file
Add an array of plugin specifiers to your config file:
```json
{
"$schema": "https://app.kilo.ai/config.json",
"plugin": [
"@your-org/your-plugin",
"your-plugin@1.2.3",
["your-plugin", { "apiKey": "{env:MY_API_KEY}" }],
"./plugins/local.ts",
"file:///abs/path/plugin.ts"
]
}
```
Each entry can be:
| Form | Loaded from |
| -------------------------------------- | ---------------------------------------------------------------- |
| `"package-name"` | Latest version from npm |
| `"package-name@1.2.3"` | Pinned version from npm |
| `["package-name", { options }]` | npm package with options passed to the plugin function |
| `"./path/plugin.ts"` / `"file:///..."` | Local file (relative to the config file or absolute `file:` URL) |
Config files live in the same locations as the rest of your CLI configuration — see the [CLI configuration reference](/docs/code-with-ai/platforms/cli#configuration).
### From a plugin directory
Drop TypeScript or JavaScript files into a `plugin/` or `plugins/` folder inside any config directory:
- Global: `~/.config/kilo/plugin/`
- Project: `.kilo/plugin/`, `.kilocode/plugin/`, or `.opencode/plugin/`
Every `.ts` or `.js` file in those directories is auto-registered at startup — no need to list them in the config file.
```text
my-project/
├── kilo.json
└── .kilo/
└── plugin/
├── env-guard.ts
└── notifications.ts
```
### From the `kilo plugin` command
Install an npm plugin and patch your config in one step:
```bash
# Install into the current project's config
kilo plugin my-plugin
# Install into your global config
kilo plugin my-plugin --global
# Replace an existing entry
kilo plugin my-plugin --force
```
The command resolves the package, reads its `package.json` for `exports["./server"]` / `exports["./tui"]`, and writes the entry into your `kilo.json` / `opencode.json` while preserving comments.
### How plugins are installed
- **npm plugins** are installed automatically at startup using Bun. Packages and their dependencies are cached in Kilo's XDG cache directory (`~/.cache/kilo/` on Linux, `~/Library/Caches/kilo/` on macOS, `%LOCALAPPDATA%\kilo\` on Windows).
- **Local plugins** are loaded directly from the plugin directory. If your plugin imports external packages, add a `package.json` to your config directory (see [Dependencies](#dependencies)) — Kilo runs `bun install` on startup so imports resolve.
### Load order
Plugins from all sources run on every session. They load in this order:
1. Internal built-ins (Kilo Gateway auth, Codex auth, Copilot auth, Cloudflare, etc.)
2. Global config plugin array (`~/.config/kilo/kilo.json`)
3. Global plugin directory (`~/.config/kilo/plugin/`)
4. Project config plugin array (`kilo.json` / `opencode.json`)
5. Project plugin directory (`.kilo/plugin/` and friends)
Duplicates (same package, same version) are deduplicated. Hooks from multiple plugins run sequentially in load order.
### Disabling external plugins
Set the `KILO_PURE=1` environment variable to skip all external plugins — only built-in plugins will load. Useful for reproducible CI runs or debugging.
---
## Create a plugin
A plugin is a module that exports a function returning a set of [hooks](#hooks-reference).
### Basic structure
Create a file in your plugin directory:
```ts
// .kilo/plugin/hello.ts
import type { Plugin } from "@kilocode/plugin"
const hello: Plugin = async ({ project, client, $, directory, worktree }) => {
console.log("hello plugin loaded")
return {
// hook implementations go here
}
}
export default { id: "hello", server: hello }
```
The plugin function receives a context object:
| Field | Description |
| ------------------------ | ----------------------------------------------------------------- |
| `project` | Current project metadata. |
| `directory` | Current working directory for this session. |
| `worktree` | Git worktree root for this session. |
| `client` | A Kilo SDK client (`@kilocode/sdk`) for calling the local server. |
| `$` | [Bun's shell API](https://bun.com/docs/runtime/shell). |
| `serverUrl` | URL of the local Kilo server. |
| `experimental_workspace` | Register workspace adaptors (used by Agent Manager). |
The function returns a `Hooks` object. Any second argument is the options object passed via config (e.g. the `{ apiKey: "..." }` from `["my-plugin", { apiKey: "..." }]`).
### Module shape
Plugins must default-export a module descriptor. `id` is required for local-file plugins and inferred from `package.json#name` for npm plugins.
```ts
import type { Plugin } from "@kilocode/plugin"
const server: Plugin = async (ctx) => ({
/* hooks */
})
export default {
id: "my-plugin",
server,
}
```
An npm plugin can also expose a TUI entry point (`tui`) for [TUI plugins](#tui-plugins), but `server` and `tui` are separate modules.
### TypeScript support
Install the plugin package locally and import its types:
```bash
bun add -d @kilocode/plugin
```
```ts
import type { Plugin } from "@kilocode/plugin"
import { tool } from "@kilocode/plugin/tool"
```
Kilo automatically creates a `package.json` in config directories that contain a `plugin/` folder and installs `@kilocode/plugin` so types resolve out of the box.
### Engine compatibility
Declare a CLI version range to prevent a plugin from loading against an incompatible build:
```json
{
"name": "my-plugin",
"engines": { "opencode": "^7.0.0" }
}
```
If the running CLI does not satisfy the range, the plugin is skipped and a warning is surfaced.
### Dependencies
Local plugins and custom tools can use external npm packages. Add a `package.json` to your config directory:
```json
// .kilo/package.json
{
"dependencies": {
"shescape": "^2.1.0"
}
}
```
Kilo runs `bun install` at startup so your plugins can import the packages:
```ts
// .kilo/plugin/escape-bash.ts
import { escape } from "shescape"
import type { Plugin } from "@kilocode/plugin"
const EscapeBash: Plugin = async () => ({
"tool.execute.before": async (input, output) => {
if (input.tool === "bash") {
output.args.command = escape(output.args.command)
}
},
})
export default { id: "escape-bash", server: EscapeBash }
```
---
## Hooks reference
Every hook is optional. Return only the ones you care about.
### Lifecycle
| Hook | Description |
| -------- | --------------------------------------------------------------------------------- |
| `config` | Receives the fully-resolved config at startup. Read-only — useful for inspection. |
| `event` | Called for **every** event on the internal bus (see [Events](#events)). |
### Tools
| Hook | Description |
| --------------------- | ----------------------------------------------------------------------------------------------- |
| `tool` | Map of tool name → [tool definition](#custom-tools). Added tools are callable by the model. |
| `tool.execute.before` | Fires before a tool runs; you can mutate `output.args`. |
| `tool.execute.after` | Fires after a tool returns; you can rewrite `output.title`, `output.output`, `output.metadata`. |
| `tool.definition` | Mutate a tool's `description` and `parameters` before they are sent to the model. |
### Chat
| Hook | Description |
| ------------------------ | ---------------------------------------------------------------------------- |
| `chat.message` | Fires when a new user message arrives. Inspect or modify `parts`. |
| `chat.params` | Mutate `temperature`, `topP`, `topK`, `maxOutputTokens`, provider `options`. |
| `chat.headers` | Add or replace HTTP headers on the LLM API call. |
| `permission.ask` | Auto-allow or auto-deny permission prompts. |
| `command.execute.before` | Intercept slash command execution; mutate the resulting `parts`. |
| `shell.env` | Inject environment variables into every shell command Kilo runs. |
### Providers & auth
| Hook | Description |
| ---------- | ------------------------------------------------------------------------------------ |
| `auth` | Register an auth method (OAuth or API key) for a provider, with interactive prompts. |
| `provider` | Dynamically supply a model catalog for a provider (useful for BYO-model gateways). |
### Experimental
These hooks live behind the `experimental.` prefix and may change between releases.
| Hook | Description |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `experimental.chat.messages.transform` | Rewrite the full message history before it is sent to the model. |
| `experimental.chat.system.transform` | Modify the system prompt array. |
| `experimental.session.compacting` | Inject extra context (`output.context`) or replace the compaction prompt entirely (`output.prompt`). |
| `experimental.compaction.autocontinue` | Disable the synthetic "continue" turn that follows compaction. |
| `experimental.text.complete` | Post-process final text parts (e.g. append signatures, redact secrets). |
### Events
The `event` hook fires for every event on Kilo's internal bus. Common event types include:
- **Session**: `session.created`, `session.updated`, `session.idle`, `session.error`, `session.deleted`, `session.compacted`, `session.diff`, `session.status`
- **Message**: `message.updated`, `message.removed`, `message.part.updated`, `message.part.removed`
- **Tool**: `tool.execute.before`, `tool.execute.after`
- **Permission**: `permission.asked`, `permission.replied`
- **File**: `file.edited`, `file.watcher.updated`
- **Shell**: `shell.env`
- **Command**: `command.executed`
- **LSP**: `lsp.updated`, `lsp.client.diagnostics`
- **Todo**: `todo.updated`
- **Server**: `server.connected`
- **Installation**: `installation.updated`
```ts
const server: Plugin = async () => ({
event: async ({ event }) => {
if (event.type === "session.idle") {
// session finished responding
}
},
})
```
---
## Custom tools
Plugins can register tools the model can call alongside the built-in ones. Use the `tool()` helper for type-safety:
```ts
// .kilo/plugin/database.ts
import type { Plugin } from "@kilocode/plugin"
import { tool } from "@kilocode/plugin/tool"
const DatabasePlugin: Plugin = async () => ({
tool: {
query: tool({
description: "Run a read-only SQL query against the project database",
args: {
sql: tool.schema.string().describe("SQL query to execute"),
},
async execute(args, context) {
const { directory, worktree } = context
// your query logic here
return `ran: ${args.sql}`
},
}),
},
})
export default { id: "database", server: DatabasePlugin }
```
`args` uses a [Zod](https://zod.dev) schema via `tool.schema`. The tool's `execute` function receives:
- `args` — validated against your schema
- `context``{ sessionID, messageID, agent, directory, worktree, abort, metadata, ask }`
### Name precedence
If a custom tool uses the same name as a built-in tool, **the custom tool wins**. Prefer unique names unless you intentionally want to override a built-in (for example, to wrap `bash` with extra validation).
### Alternative: standalone tool files
For tools that don't need the full plugin context, drop them in a `tool/` or `tools/` folder inside any config directory — for example `.kilo/tool/database.ts` or `~/.config/kilo/tool/database.ts`. The filename becomes the tool name, and each file exports a `tool()` definition directly. The layout is identical to the [OpenCode custom tools guide](https://opencode.ai/docs/custom-tools); substitute `.kilo/` (or `.kilocode/` / `.opencode/`) for `.opencode/`.
---
## Examples
### Send a notification when a session finishes
```ts
// .kilo/plugin/notify.ts
import type { Plugin } from "@kilocode/plugin"
const Notify: Plugin = async ({ $ }) => ({
event: async ({ event }) => {
if (event.type === "session.idle") {
await $`osascript -e 'display notification "Session complete!" with title "Kilo"'`
}
},
})
export default { id: "notify", server: Notify }
```
{% callout type="tip" %}
The VS Code extension already emits system notifications when a session finishes or errors — this plugin is for the raw CLI / TUI.
{% /callout %}
### Block reads of `.env` files
```ts
// .kilo/plugin/env-guard.ts
import type { Plugin } from "@kilocode/plugin"
const EnvGuard: Plugin = async () => ({
"tool.execute.before": async (input, output) => {
if (input.tool === "read" && String(output.args.filePath).includes(".env")) {
throw new Error("reading .env files is blocked")
}
},
})
export default { id: "env-guard", server: EnvGuard }
```
### Inject environment variables into every shell command
```ts
// .kilo/plugin/inject-env.ts
import type { Plugin } from "@kilocode/plugin"
const InjectEnv: Plugin = async () => ({
"shell.env": async (input, output) => {
output.env.MY_API_KEY = "secret"
output.env.PROJECT_ROOT = input.cwd
},
})
export default { id: "inject-env", server: InjectEnv }
```
### Structured logging
Prefer `client.app.log()` over `console.log` so entries land in Kilo's log pipeline:
```ts
import type { Plugin } from "@kilocode/plugin"
const Logger: Plugin = async ({ client }) => {
await client.app.log({
body: {
service: "my-plugin",
level: "info",
message: "plugin initialized",
extra: { version: "1.0.0" },
},
})
return {}
}
export default { id: "logger", server: Logger }
```
Levels: `debug`, `info`, `warn`, `error`.
### Inject context during session compaction
```ts
// .kilo/plugin/compaction.ts
import type { Plugin } from "@kilocode/plugin"
const Compaction: Plugin = async () => ({
"experimental.session.compacting": async (input, output) => {
output.context.push(
"## Persist across compaction\n- current task status\n- files being actively edited\n- key decisions",
)
},
})
export default { id: "compaction", server: Compaction }
```
Set `output.prompt` to replace the default compaction prompt entirely — when present, `output.context` is ignored.
---
## TUI plugins
Plugins can also target the Kilo TUI itself — registering slash commands, routes, sidebar slots, dialogs, and keybinds. TUI plugins are SolidJS modules exported from `"./tui"` in your plugin package.
TUI plugins live in a separate module namespace (`@kilocode/plugin/tui`) and have their own API surface (`TuiPluginApi`). Because the TUI API is larger and still evolving, this guide doesn't cover it exhaustively — use the types in `@kilocode/plugin/tui` as the reference, and look at the built-in TUI plugins under `packages/opencode/src/cli/cmd/tui/feature-plugins/` for working examples.
---
## Troubleshooting
- **Plugin failed to load** — check the CLI logs with `kilo --print-logs --log-level DEBUG`. Load failures are also surfaced as session errors in the TUI and VS Code extension.
- **Plugin loaded but hooks never fire** — make sure the default export includes `server`:
```ts
export default { id: "my-plugin", server }
```
Named function exports are also accepted for backwards compatibility but should be considered legacy.
- **Local plugin can't find an npm import** — add a `package.json` in the config directory so `bun install` picks up the dependency (see [Dependencies](#dependencies)).
- **Plugin loads in dev but not in CI** — verify `KILO_PURE` is not set, and that npm-installed plugins are cached (Kilo's XDG cache directory — `~/.cache/kilo/` on Linux, `~/Library/Caches/kilo/` on macOS, `%LOCALAPPDATA%\kilo\` on Windows). Run with `--log-level DEBUG` to see install output.
- **Reset the plugin cache** — delete the `node_modules/` under Kilo's cache directory (or the `node_modules` cache under your config directory) and restart Kilo.
---
## Reference
- Types: [`@kilocode/plugin`](https://github.com/Kilo-Org/kilocode/tree/main/packages/plugin) — `Plugin`, `Hooks`, `PluginInput`, `ToolDefinition`, `AuthHook`, `ProviderHook`.
- Example plugin: [`packages/plugin/src/example.ts`](https://github.com/Kilo-Org/kilocode/blob/main/packages/plugin/src/example.ts)
- CLI command: [`kilo plugin`](/docs/code-with-ai/platforms/cli-reference#kilo-plugin)
- Upstream docs (behavior is identical to OpenCode): [opencode.ai/docs/plugins](https://opencode.ai/docs/plugins) and [opencode.ai/docs/custom-tools](https://opencode.ai/docs/custom-tools)