Pi Agent Extension Getting Started
Extensions are the most powerful way to customize Pi Agent—you can write custom tools, commands, and event listeners in TypeScript.
What is an Extension
An Extension is a TypeScript module that can do the following.
| Capability | Description | Typical Use |
|---|---|---|
| RegisterCustom Tools | Add new callable functions for AI | Query internal systems, read/write databases, wrap third-party APIs |
| RegisterCustom Commands | Invoked by users via /command-name | Wrap common prompts, execute fixed workflows with one click |
| ListenLifecycle Events | Insert logic at various stages of AI work | Intercept dangerous commands, inject context, record audit logs |
| RegisterKeyboard Shortcuts | Bind keys in the terminal UI | Quickly switch panels, trigger high-frequency operations |
| DisplayCustom UI | Render interactive interfaces in the terminal | Components such as progress bars, lists, and confirmation dialogs |
Your First Extension: Hello World
The following extension demonstrates three capabilities at once: event listening, tool registration, and command registration.
Example
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// Triggered when the session starts
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("My extension has loaded!", "info");
});
// Intercept dangerous commands
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash"
&& event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("Dangerous operation!", "Are you sure you want to run rm -rf?");
if (!ok) return { block: true, reason: "User blocked" };
}
});
// Register a custom tool
pi.registerTool({
name: "greet",
label: "Greet",
description: "Greet the specified person",
parameters: Type.Object({
name: Type.String({ description: "The name of the person to greet" }),
}),
async execute(toolCallId, params) {
return {
content: [{ type: "text", text:`Hello, ${params.name}!` }],
details: {},
};
},
});
// Register a custom command
pi.registerCommand("hello", {
description: "Greet the world",
handler: async (args, ctx) => {
ctx.ui.notify(`Hello ${args || "World"}!`, "info");
},
});
}
The event check on line 13 uses the simplestevent.toolNameapproach; you can also use type guards to narrow the event to a specific event type, see the Event System chapter.
Start Pi Agent with the pi -e parameter to load this extension for testing:
$ pi -e ~/.pi/agent/extensions/my-extension.ts Loading extension: /Users/example/.pi/agent/extensions/my-extension.ts my-extension 加载成功(1 个工具、1 个命令) 我的扩展已加载!
The last line is the notification popped up by ctx.ui.notify in the session_start event, which indicates the extension has taken effect.
Extension Loading Locations
Pi Agent automatically discovers extensions in several fixed directories, and you can also explicitly specify paths.
| Location | Scope | Description |
|---|---|---|
| ~/.pi/agent/extensions/*.ts | Global | Effective for all projects, auto-discovered |
| ~/.pi/agent/extensions/*/index.ts | Global | Extensions with index.ts in subdirectories |
| .pi/extensions/*.ts | Project | Current project only, requires project trust |
| .pi/extensions/*/index.ts | Project | Project subdirectory extensions |
In addition to command-line arguments, you can also write an array of paths in the extensions field of settings.json; directory paths are expanded according to the extension directory rules.
Example
"extensions": [
"/Users/example/.pi/agent/extensions/quick-notify.ts",
"/Users/example/.pi/agent/extensions/team-tools"
]
}
The first element is a single-file extension, and the second is a directory extension; replace them with your own extension locations in actual use.
Extension File Structure
An Extension can be a single .ts file or a directory with an entry file.
Single-file Extension
The simplest form, suitable for small extensions:
~/.pi/agent/extensions/ └── my-extension.ts
Directory Extension (with index.ts)
Suitable for multi-file extensions:
~/.pi/agent/extensions/
└── my-extension/
├── index.ts # 入口文件(导出默认函数)
├── tools.ts # 辅助模块
└── utils.ts # 工具函数
Extension with Dependencies
Suitable for large extensions that require npm packages:
~/.pi/agent/extensions/
└── my-extension/
├── package.json # 声明依赖和入口
├── node_modules/ # npm install 后生成
└── src/
└── index.ts
Extensions run with your system permissions and can execute arbitrary code.
Only install extensions from trusted sources.。
Extension Loading Mechanism
Extensions are loaded viajitiloading; TypeScript code can run directly without compilation.
If your factory function returns a Promise, Pi Agent waits for it to complete before continuing the startup process.
This means you can perform asynchronous operations during extension initialization, such as fetching remote configuration.
Available Import Packages
These packages are built into Pi Agent and can be imported directly in extensions.
| Package Name | Purpose |
|---|---|
| @earendil-works/pi-coding-agent | Extension types (ExtensionAPI, ExtensionContext, and various event types) |
| typebox | Schema definition for tool parameters |
| @earendil-works/pi-ai | AI tools (such as StringEnum, for Google-compatible enums) |
| @earendil-works/pi-tui | TUI components, for custom rendering |
These built-in core packages are provided by Pi itself; just list them as peerDependencies, and do not bundle them into your extension.
Your own runtime dependencies must be written in the dependencies field of package.json.
When Pi installs extension packages, it performs a production install by default (npm install --omit=dev), so devDependencies are not available at runtime.
Other npm packages can also be used—add a package.json in the extension directory and run npm install.
Development Tips
These pieces of experience can help you avoid detours, especially when choosing the loading method.
| Approach | Reason |
|---|---|
| Put the extension in~/.pi/agent/extensions/the directory | Supports /reload hot reload, no need to restart the session after changes |
| pi -e ./path.tsOnly for temporary testing | For long-term use, put the extension in the ~/.pi/agent/extensions/ directory and let Pi Agent auto-discover it. |
| Do not start background resources in the extension factory function | Processes, sockets, file watchers, timers, etc. should be started in the session_start event |
| Clean up session-level resources in the session_shutdown event | Avoid leftover handles and listeners after switching sessions |
Further Integration
Besides writing extensions, the official team also provides two programmatic integration routes.
Node or TypeScript projects can directly use@earendil-works/pi-coding-agentthe exported SDK: use createAgentSession to create sessions, subscribe to events, and prompt to send messages.
This approach runs in the same process as Pi and has complete type definitions.
For cross-language calls or when process isolation is needed, switch topi --mode rpc, and communicate with the Pi process via the JSONL protocol.
Both routes share the same event model as extensions; which one to choose mainly depends on whether you need an independent process.
Other Extensions