graph TD
A[Startup] --> B[resources_discover]
B --> C[project_trust]
C --> D[session_start]
D --> E[User message]
E --> F[Assistant turn]
F --> G[tool_call]
G --> H[tool_result]
H --> F
F --> I[Turn end]
I --> E
E --> J[session_shutdown]
13 Extensions
13.1 Quick Start
Extensions are TypeScript modules that extend Pi’s behavior. They can subscribe to lifecycle events, register custom tools callable by the LLM, add commands, and more.
Create ~/.pi/agent/extensions/my-extension.ts:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// React to events
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Extension loaded!", "info");
});
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
if (!ok) return { block: true, reason: "Blocked by user" };
}
});
// Register a custom tool
pi.registerTool({
name: "greet",
label: "Greet",
description: "Greet someone by name",
parameters: Type.Object({
name: Type.String({ description: "Name to greet" }),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
return {
content: [{ type: "text", text: `Hello, ${params.name}!` }],
details: {},
};
},
});
// Register a command
pi.registerCommand("hello", {
description: "Say hello",
handler: async (args, ctx) => {
ctx.ui.notify(`Hello ${args || "world"}!`, "info");
},
});
}Test with --extension (or -e) flag:
pi -e ./my-extension.tsExtensions run with your full system permissions and can execute arbitrary code. Only install from sources you trust.
13.2 Extension Locations
Extensions are auto-discovered from trusted locations. Project-local .pi/extensions entries load only after the project is trusted.
| Location | Scope |
|---|---|
~/.pi/agent/extensions/*.ts |
Global (all projects) |
~/.pi/agent/extensions/*/index.ts |
Global (subdirectory) |
.pi/extensions/*.ts |
Project-local |
.pi/extensions/*/index.ts |
Project-local (subdirectory) |
Additional paths via settings.json:
{
"extensions": [
"/path/to/local/extension.ts",
"/path/to/local/extension/dir"
]
}Extensions in auto-discovered locations can be hot-reloaded with /reload.
13.3 Available Imports
| Package | Purpose |
|---|---|
@earendil-works/pi-coding-agent |
Extension types (ExtensionAPI, ExtensionContext, events) |
typebox |
Schema definitions for tool parameters |
@earendil-works/pi-ai |
AI utilities (StringEnum for Google-compatible enums) |
@earendil-works/pi-tui |
TUI components for custom rendering |
npm dependencies work too. Add a package.json next to your extension, run npm install, and imports from node_modules/ are resolved automatically. Node.js built-ins (node:fs, node:path, etc.) are also available.
13.4 Writing an Extension
An extension exports a default factory function that receives ExtensionAPI. The factory can be synchronous or asynchronous:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.on("event_name", async (event, ctx) => {
const ok = await ctx.ui.confirm("Title", "Are you sure?");
ctx.ui.notify("Done!", "info");
ctx.ui.setStatus("my-ext", "Processing...");
ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]);
});
pi.registerTool({ /* ... */ });
pi.registerCommand("name", { /* ... */ });
pi.registerShortcut("ctrl+x", { /* ... */ });
pi.registerFlag("my-flag", { /* ... */ });
}Extensions are loaded via jiti, so TypeScript works without compilation. If the factory returns a Promise, Pi awaits it before continuing startup.
13.4.1 Async Factory Functions
Use an async factory for one-time startup work such as fetching remote configuration:
export default async function (pi: ExtensionAPI) {
const response = await fetch("http://localhost:1234/v1/models");
const payload = await response.json();
pi.registerProvider("local-openai", {
baseUrl: "http://localhost:1234/v1",
apiKey: "$LOCAL_OPENAI_API_KEY",
api: "openai-completions",
models: payload.data.map((model) => ({
id: model.id,
name: model.name ?? model.id,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: model.context_window ?? 128000,
maxTokens: model.max_tokens ?? 4096,
})),
});
}13.4.2 Long-Lived Resources and Shutdown
Extension factories may run in invocations that never start a session. Do not start background resources from the factory. Defer resource startup until session_start or the command/tool/event that needs it. Register an idempotent session_shutdown handler to close session-scoped resources.
13.5 Extension Styles
13.5.1 Single File
~/.pi/agent/extensions/
└── my-extension.ts
13.5.2 Directory with index.ts
~/.pi/agent/extensions/
└── my-extension/
├── index.ts
├── tools.ts
└── utils.ts
13.5.3 Package with Dependencies
~/.pi/agent/extensions/
└── my-extension/
├── package.json
├── package-lock.json
├── index.ts
└── node_modules/
13.6 Events
13.6.1 Lifecycle Overview
13.6.2 Startup Events
| Event | Description |
|---|---|
resources_discover |
After settings and resources are loaded; before trust check |
project_trust |
When a project trust decision is needed; first extension returning a decision wins |
session_start |
After the session is loaded or created and context is built |
session_shutdown |
Before the session exits; cleanup resources here |
13.6.3 Resource Events
| Event | Description |
|---|---|
resources_discover |
Discover extensions, skills, prompts, themes |
13.6.4 Session Events
| Event | Description |
|---|---|
session_start |
Session loaded or created |
session_shutdown |
Session about to exit |
session_saved |
Session file written to disk |
13.6.5 Agent Events
| Event | Description |
|---|---|
message_queued |
A message was queued while agent is busy |
message_delivered |
A queued message was delivered |
before_user_message |
Before sending user message to model |
after_user_message |
After user message is sent |
assistant_response |
Assistant produced output |
turn_start |
Agent turn started |
turn_end |
Agent turn ended |
13.6.6 Model Events
| Event | Description |
|---|---|
model_start |
Model API call started |
model_end |
Model API call completed |
model_error |
Model API call failed |
compaction:before |
Before context compaction |
compaction:after |
After context compaction |
13.6.7 Tool Events
| Event | Description |
|---|---|
tool_call |
Before a tool is called; can block or modify |
tool_result |
After a tool returns; can modify result |
Tool call handlers can return { block: true, reason: "..." } to prevent execution, or { input: {...} } to modify parameters.
13.6.8 User Bash Events
| Event | Description |
|---|---|
user_bash |
User ran !command in the editor |
13.6.9 Input Events
| Event | Description |
|---|---|
input_submit |
User submitted input in the editor |
13.7 ExtensionContext
The ExtensionContext (ctx) provides access to Pi’s internal state and UI:
13.7.1 ctx.ui
User interaction methods: notify(), confirm(), select(), input(), setStatus(), setWidget(), custom().
13.7.2 ctx.mode
Current run mode: "tui", "json", "rpc", or "print".
13.7.3 ctx.hasUI
Whether a TUI is available (only true in "tui" mode).
13.7.4 ctx.cwd
Current working directory.
13.7.5 ctx.isProjectTrusted()
Whether the project has been trusted.
13.7.6 ctx.sessionManager
Access to the session manager for reading and manipulating sessions.
13.7.7 ctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels
Access to the model registry, current model, thinking level, and scoped models.
13.7.8 ctx.signal
AbortSignal for cooperative cancellation.
13.7.9 ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()
Check if agent is idle, abort current work, or check for queued messages.
13.7.10 ctx.shutdown()
Trigger session shutdown.
13.7.11 ctx.getContextUsage()
Current context window usage (tokens used, percentage).
13.7.12 ctx.compact(options?)
Trigger manual compaction.
13.7.13 ctx.getSystemPrompt()
Get the current system prompt.
13.8 ExtensionCommandContext
When inside a command handler, the context includes additional methods:
13.8.1 ctx.getSystemPromptOptions()
Get current system prompt options.
13.8.2 ctx.waitForIdle()
Wait for the agent to become idle.
13.8.3 ctx.newSession(options?)
Create a new session.
13.8.4 ctx.fork(entryId, options?)
Fork the session from a specific entry.
13.8.6 ctx.switchSession(sessionPath, options?)
Switch to a different session file.
13.8.7 ctx.reload()
Reload extensions, skills, prompts, themes, and context files.
13.9 ExtensionAPI Methods
13.9.1 pi.on(event, handler)
Register an event handler.
13.9.2 pi.registerTool(definition)
Register a custom tool callable by the LLM.
13.9.3 pi.sendMessage(message, options?)
Send a message to the model programmatically.
13.9.4 pi.sendUserMessage(content, options?)
Send a user message programmatically.
13.9.5 pi.appendEntry(customType, data?)
Append a custom entry to the session file for persistence.
13.9.6 pi.setSessionName(name) / pi.getSessionName()
Set or get the session display name.
13.9.7 pi.setLabel(entryId, label)
Set a custom label on a session entry.
13.9.8 pi.registerCommand(name, options)
Register a slash command.
13.9.9 pi.getCommands()
Get all registered commands.
13.9.10 pi.registerMessageRenderer(customType, renderer)
Custom rendering for messages of a custom type.
13.9.11 pi.registerMarkdownTransformer(transformer)
Transform markdown before rendering.
13.9.12 pi.registerEntryRenderer(customType, renderer)
Custom rendering for session entries.
13.9.13 pi.registerShortcut(shortcut, options)
Register a keyboard shortcut.
13.9.14 pi.registerFlag(name, options)
Register a CLI flag.
13.9.15 pi.exec(command, args, options?)
Execute a shell command.
13.9.16 pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)
Manage the active tool set.
13.9.17 pi.setModel(model) / pi.getThinkingLevel() / pi.setThinkingLevel(level)
Manage the current model and thinking level.
13.9.18 pi.events
Direct access to the event emitter.
13.9.19 pi.registerProvider(name, config) / pi.unregisterProvider(name)
Register or unregister a custom provider.
13.10 State Management
Extensions can persist state across restarts by appending custom entries to the session file:
pi.appendEntry("my-ext:state", {
counter: 42,
lastAction: "greet",
items: ["apple", "banana"],
});On session_start, read custom entries from the session tree to restore state.
13.11 Custom Tools
13.11.1 Tool Definition
pi.registerTool({
name: "search_files",
label: "Search",
description: "Search file contents for a pattern",
parameters: Type.Object({
pattern: Type.String({ description: "Regex pattern to search for" }),
path: Type.Optional(Type.String({ description: "Directory to search in" })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
// Implementation
return {
content: [{ type: "text", text: "Found 3 matches..." }],
details: { matchCount: 3 },
};
},
});13.11.2 Overriding Built-in Tools
Register a tool with the same name as a built-in to override it. The tool name must match exactly.
13.11.3 Remote Execution
Tools can execute code on remote systems:
pi.registerTool({
name: "deploy",
description: "Deploy to staging",
parameters: Type.Object({}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const result = await fetch("https://ci.example.com/deploy", { /* ... */ });
return { content: [{ type: "text", text: "Deployed!" }], details: {} };
},
});13.11.4 Dynamic Tool Loading
Tools can be registered and unregistered at runtime:
pi.on("session_start", async (_event, ctx) => {
if (await shouldEnableFeature()) {
pi.registerTool({ /* ... */ });
}
});13.12 Custom UI
13.12.1 Dialogs
const choice = await ctx.ui.select("Choose option", [
"Option A",
"Option B",
"Option C",
]);
const confirmed = await ctx.ui.confirm("Delete files?", "This cannot be undone.");
const name = await ctx.ui.input("Enter name:", { placeholder: "John Doe" });13.12.3 Autocomplete Providers
Register custom autocomplete providers for the @ file reference menu or Tab completion.
13.12.4 Custom Components
For complex interactions, use ctx.ui.custom() to render a full custom TUI component with keyboard input handling.
13.13 Error Handling
Extension errors are caught by Pi. Tool errors are returned to the model as error results. Event handler errors are logged and displayed as notifications. Extensions should handle expected errors gracefully:
async execute(toolCallId, params, signal, onUpdate, ctx) {
try {
const result = await riskyOperation();
return { content: [{ type: "text", text: result }], details: {} };
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
details: { error: true },
};
}
}13.14 Mode Behavior
Extensions behave differently depending on the run mode:
| Mode | UI Available | Events | Tools |
|---|---|---|---|
tui |
Full UI | All events | Full interaction |
print |
No UI | Most events | No interactive UI |
json |
No UI | Most events | No interactive UI |
rpc |
No UI | Most events | No interactive UI |
Always check ctx.hasUI before calling UI methods. In non-interactive modes, ctx.ui methods are no-ops or return defaults.
See examples/extensions/ in the Pi repository for working implementations including permission gates, git checkpointing, path protection, conversation summaries, interactive tools, and even games.