Extensions (TypeScript)
✓ Fresh · 2026-02-24Extensions are TypeScript modules that add custom tools, commands, shortcuts, event handlers, and UI elements to Pi. This is Pi's primary extensibility mechanism.
What Extensions Can Do
Full capabilities list:
- Custom and replacement tools
- Sub-agents
- Plan mode
- Custom compaction logic
- Permission gates and confirmation flows
- Custom editors
- Status lines and widgets
- Git checkpointing
- SSH and sandbox execution
- MCP integration
- Games and utilities
Creating an Extension
An extension is a TypeScript file that exports a default function receiving the ExtensionAPI:
typescript
export default function (pi: ExtensionAPI) {
// Register a custom tool
pi.registerTool({
name: "deploy",
description: "Deploy the application to production",
parameters: {
type: "object",
properties: {
environment: {
type: "string",
enum: ["staging", "production"],
description: "Target environment"
}
},
required: ["environment"]
},
async execute(args) {
const { environment } = args;
// Your deployment logic here
return `Deployed to ${environment}`;
}
});
// Register a custom command
pi.registerCommand("stats", {
description: "Show session statistics",
async execute() {
// Your stats logic here
}
});
// Hook into events
pi.on("tool_call", async (event, ctx) => {
console.log(`Tool called: ${event.tool}`);
// Modify, log, or gate tool calls
});
}Loading Extensions
Command Line
bash
# Single extension
pi -e extensions/my-ext.ts
# Stack multiple extensions (loaded in order)
pi -e ext1.ts -e ext2.ts
# Combine with session flags
pi -c -e extensions/damage-control.ts -e extensions/tool-counter.tsAuto-Loading
Extensions placed in these directories are loaded automatically:
| Directory | Scope |
|---|---|
~/.pi/agent/extensions/ | Global (all projects) |
.pi/extensions/ | Project-specific |
| Pi packages | Installed via pi install |
Using just Recipes
Create a justfile for convenient extension combos:
just
# Plain Pi, no extensions
pi:
pi
# Pi with specific extension
ext name:
pi -e extensions/{{name}}.ts
# Combine multiple extensions
full:
pi -e extensions/damage-control.ts -e extensions/tool-counter.ts -e extensions/minimal.tsExtension API Reference
pi.registerTool(toolDef)
Register a new tool the agent can call:
typescript
pi.registerTool({
name: string,
description: string,
parameters: JSONSchema,
execute: (args: any) => Promise<string>
});pi.registerCommand(name, commandDef)
Register a slash command:
typescript
pi.registerCommand("mycommand", {
description: string,
execute: () => Promise<void>
});pi.on(event, handler)
Subscribe to lifecycle events:
typescript
pi.on("tool_call", async (event, ctx) => {
// event contains tool name, arguments, etc.
// ctx provides session context
});
pi.on("session_start", async (event) => {
// Runs when a session begins
});See Hooks & Events for the complete event list.
Extension Loading Order
Best Practices
- Single responsibility — Each extension should do one thing well
- Use events — Hook into lifecycle events rather than replacing core behavior
- Stack extensions — Combine multiple focused extensions instead of building monoliths
- Share via packages — Bundle extensions as Pi packages for easy distribution
- Hot reload — Use
/reloadduring development to refresh extensions without restarting
Example: Permission Gate
typescript
export default function (pi: ExtensionAPI) {
pi.on("tool_call", async (event, ctx) => {
if (event.tool === "bash" && event.args.command.includes("rm")) {
const confirmed = await ctx.confirm(
`Dangerous command detected: ${event.args.command}\nAllow?`
);
if (!confirmed) {
return { abort: true, message: "Command blocked by permission gate." };
}
}
});
}Example: Custom Status Line
typescript
export default function (pi: ExtensionAPI) {
let toolCount = 0;
pi.on("tool_call", async () => {
toolCount++;
pi.setStatusLine(`Tools used: ${toolCount}`);
});
pi.on("session_start", async () => {
toolCount = 0;
pi.setStatusLine("Tools used: 0");
});
}