Building extensions
TypeScript modules with event hooks, custom tools, slash commands and TUI widgets.
The mental model
A pi extension is a single TypeScript (or JavaScript) module that exports a default function. pi
calls it once at startup with an ExtensionAPI object, and from there you can hook lifecycle
events, register tools the LLM can call, add slash commands and keyboard shortcuts, draw TUI
widgets, and even rewrite or block tool calls before they run.
No build step. Drop a .ts file in the right directory and /reload.
Hello, extension
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// React to lifecycle events
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Extension loaded!", "info");
});
// Give the model a new tool
pi.registerTool({
name: "greet",
description: "Greet someone",
parameters: Type.Object({ name: Type.String() }),
async execute(toolCallId, params, signal, onUpdate, ctx) {
return { content: [{ type: "text", text: `Hello, ${params.name}!` }] };
},
});
// Add a slash command for the human
pi.registerCommand("hello", {
handler: async (args, ctx) => {
ctx.ui.notify(`Hello ${args || "world"}!`, "info");
},
});
}
Try it instantly
pi -e ./my-extension.ts
For permanent use, put it in ~/.pi/agent/extensions/ (global) or .pi/extensions/ (per
project). Both accept single files or folders with an index.ts. Auto-discovered extensions
hot-reload with /reload.
What the API gives you
- Events — subscribe with
pi.on(...)to hooks likesession_start,before_agent_start,input,tool_callandmodel_selectto observe or modify behavior at each step. - Tools —
pi.registerTool()adds model-callable tools with typed parameters, streaming updates and custom rendering. You can also override or wrap the built-in tools — that’s how sandboxing and permission extensions work. - Commands & shortcuts —
pi.registerCommand("mycommand", ...)adds/mycommand; keyboard shortcuts likectrl+xare supported too. - UI —
ctx.uioffers dialogs (select,confirm,input), notifications, status indicators and persistent custom widgets. - Messages — inject context for the model with
pi.sendMessage()or simulate user input withpi.sendUserMessage(). - State — persist entries into the session with
pi.appendEntry()and rebuild your state onsession_start, so your extension survives restarts and/reload.
Ground rules worth knowing
- Truncate tool output. pi’s built-in tools cap output around 50KB / 2,000 lines to protect the context window — yours should too.
- Tools run in parallel by default. If your tool mutates files, serialize with the provided file-mutation queue helper to avoid lost writes.
- Sessions can be replaced (e.g. after
await ctx.reload()); don’t reuse stale context objects across that boundary.
Study working code
The fastest way to learn is reading small real extensions:
- pi-powerline-footer — custom footer UI
- pi-web-access — a clean multi-tool extension
- tmustier/pi-extensions — many small, delightful examples
- pi-doom — proof there are no real limits
When it works, package it and publish.