Plugins
Extend Kodine with custom plugins.
Plugins tap into Kodine’s event stream to reshape how it behaves — new features, third-party integrations, or tweaks to the defaults are all on the table.
The community has published plenty of plugins worth browsing.
Use a plugin
Plugins load from two kinds of sources.
From local files
Drop JavaScript or TypeScript files into a plugin directory.
.kodine/plugins/— plugins scoped to the project~/.config/kodine/plugins/— plugins available everywhere
Anything in these folders is picked up at startup.
From npm
Reference npm packages straight from the config file.
{ "$schema": "https://kodine.net/config.json", "plugin": ["kodine-helicone-session", "kodine-wakatime", "@my-org/custom-plugin"]}Plain and scoped package names both work.
See what’s available in the ecosystem.
How plugins are installed
npm plugins install themselves via Bun on startup, with packages and their dependencies cached under ~/.cache/kodine/node_modules/.
Local plugins load straight from the plugin directory. External packages require a package.json inside your config directory (see Dependencies) — or publish the plugin to npm and add it to your config.
Load order
Every source is loaded, and all hooks fire in sequence. The order is:
- Global config (
~/.config/kodine/kodine.json) - Project config (
kodine.json) - Global plugin directory (
~/.config/kodine/plugins/) - Project plugin directory (
.kodine/plugins/)
An npm package appearing twice with identical name and version loads only once. A local plugin and an npm plugin that merely share a similar name, however, are treated as two distinct plugins.
Create a plugin
At its core a plugin is a JavaScript/TypeScript module exporting one or more plugin functions. Each function takes a context object and hands back a hooks object.
Dependencies
External npm packages work in local plugins and custom tools alike. Declare what you need in a package.json inside your config directory.
{ "dependencies": { "shescape": "^2.1.0" }}On startup Kodine runs bun install over that file, after which your plugins and tools can import the packages freely.
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
export const MyPlugin = async ({ project, client, $, directory, worktree }) => { console.log("Plugin initialized!")
return { // Hook implementations go here }}The plugin function is handed:
project: details about the active project.directory: the working directory.worktree: the git worktree location.client: a Kodine SDK client for talking to the AI.$: Bun’s shell API for running commands.
TypeScript support
TypeScript users can pull types from the plugin package:
import type { Plugin } from "@kodine-ai/plugin"
export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => { return { // Type-safe hook implementations }}Events
As the Examples section below shows, plugins can listen for events. The full catalog:
Command Events
command.executed
File Events
file.editedfile.watcher.updated
Installation Events
installation.updated
LSP Events
lsp.client.diagnosticslsp.updated
Message Events
message.part.removedmessage.part.updatedmessage.removedmessage.updated
Permission Events
permission.askedpermission.replied
Server Events
server.connected
Session Events
session.createdsession.compactedsession.deletedsession.diffsession.errorsession.idlesession.statussession.updated
Todo Events
todo.updated
Shell Events
shell.env
Tool Events
tool.execute.aftertool.execute.before
TUI Events
tui.prompt.appendtui.command.executetui.toast.show
Examples
A few plugins to spark ideas.
Send notifications
Fire notifications when particular events occur:
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 "kodine"'` } }, }}This leans on osascript, macOS’s AppleScript runner, to surface the notification.
.env protection
Stop Kodine from opening .env files:
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
Push environment variables into every shell invocation (AI tools and user terminals included):
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 register brand-new tools too:
import { type Plugin, tool } from "@kodine-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 builds a tool Kodine can call. Feed it a definition containing:
description: a summary of the tool’s purposeargs: a Zod schema describing the tool’s argumentsexecute: the function invoked on each call
From then on, your custom tool sits next to the built-in ones.
Logging
Prefer client.app.log() over console.log when you want structured logging:
export const MyPlugin = async ({ client }) => { await client.app.log({ body: { service: "my-plugin", level: "info", message: "Plugin initialized", extra: { foo: "bar" }, }, })}Available levels: debug, info, warn, error. The SDK documentation has the specifics.
Compaction hooks
Shape what context survives when a session is compacted:
import type { Plugin } from "@kodine-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`) }, }}experimental.session.compacting runs just before the LLM writes its continuation summary — your chance to slip in domain-specific context the stock prompt would overlook.
Setting output.prompt goes further and swaps out the whole compaction prompt:
import type { Plugin } from "@kodine-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.` }, }}With output.prompt present, the default compaction prompt is fully replaced and the output.context array gets ignored.