Authoring TypeScript extensions for the pi-coding-agent (lifecycle hooks, custom tools, UI, providers). Use when working with pi.dev extensions features.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill pi-extensions-expert --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Pi Extensions Expert?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-pi-extensions-expert)More formats (shields.io, HTML) on the badges page.
---
name: pi-extensions-expert
description: Authoring TypeScript extensions for the pi-coding-agent (lifecycle hooks, custom tools, UI, providers). Use when working with pi.dev extensions features.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: pi-dev
tags: [pi-dev, pi-coding-agent, extensions, typescript, agent-tooling]
---
# Pi Extensions Expert Mode
You are an expert in authoring extensions for the pi-coding-agent. You write TypeScript modules that hook into the agent's lifecycle, register custom tools, drive the TUI, and override built-in behavior. You know the full `ExtensionAPI` and `ExtensionContext` surface and the order events fire in.
## Core Concepts
Pi extensions are TypeScript modules that enhance pi-coding-agent's behavior through lifecycle events, custom tools, commands, shortcuts, providers, and UI interactions. They run with full system permissions and execute arbitrary code, so they must only be installed from trusted sources.
Extensions auto-discover from:
- `~/.pi/agent/extensions/*.ts` (global, single file)
- `~/.pi/agent/extensions/*/index.ts` (global, directory)
- `.pi/extensions/*.ts` (project-local)
- `.pi/extensions/*/index.ts` (project-local directory)
Additional paths can be set in `settings.json`:
```json
{ "extensions": ["/path/to/local/extension.ts"] }
```
For quick iteration: `pi -e ./my-extension.ts`.
Available imports:
- `@mariozechner/pi-coding-agent` — `ExtensionAPI`, helper utilities
- `typebox` — schemas (`Type.Object`, `Type.Optional`, etc.)
- `@mariozechner/pi-ai` — `StringEnum` and AI utilities
- `@mariozechner/pi-tui` — TUI components (e.g. `Text`)
- Node built-ins (`node:fs`, `node:path`) and any local npm deps
## Authoring Patterns
Extensions export a default factory (sync or async):
```typescript
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Loaded!", "info");
});
pi.registerTool({ /* ... */ });
pi.registerCommand("name", { /* ... */ });
}
```
Async factories support one-time startup work (e.g. probing a local LLM endpoint to register discovered models dynamically).
### Event Lifecycle (in order)
1. **Startup**: `session_start` → `resources_discover`
2. **User input**: `input` (interceptable) → `before_agent_start` → `agent_start`
3. **Agent loop**: `turn_start` → LLM tool calls → `tool_call` (blockable) → `tool_result` (mutable) → `turn_end`
4. **Messages**: `message_start` / `message_update` / `message_end`
5. **Session changes**: `session_before_switch` → `session_shutdown` → `session_start`
6. **Compaction**: `session_before_compact` / `session_compact`
7. **Exit**: `session_shutdown`
Other events: `context` (per LLM call), `model_select`, `thinking_level_select`, `user_bash` (`!`/`!!`), `session_before_tree` / `session_tree`.
### Intercepting Tool Calls (block or mutate)
```typescript
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command.includes("rm -rf")) {
return { block: true, reason: "Dangerous" };
}
event.input.command = `source ~/.profile\n${event.input.command}`;
});
```
### Injecting Context Before Agent Start
```typescript
pi.on("before_agent_start", async (event, ctx) => {
return {
message: { customType: "my-ext", content: "Context" },
systemPrompt: event.systemPrompt + "\nExtra instructions",
};
});
```
### Intercepting Input
```typescript
pi.on("input", async (event, ctx) => {
if (event.text === "ping") {
ctx.ui.notify("pong", "info");
return { action: "handled" };
}
return { action: "continue" };
});
```
### Registering a Custom Tool
```typescript
import { Type } from "typebox";
import { StringEnum } from "@mariozechner/pi-ai";
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What it does",
promptSnippet: "Short description for system prompt",
promptGuidelines: ["Use my_tool when..."],
parameters: Type.Object({
action: StringEnum(["list", "add"] as const),
text: Type.Optional(Type.String()),
}),
prepareArguments(args) { return args; },
async execute(toolCallId, params, signal, onUpdate, ctx) {
onUpdate?.({ content: [{ type: "text", text: "Progress" }] });
return {
content: [{ type: "text", text: "Result" }],
details: { data: "..." },
terminate: true,
};
},
renderCall(args, theme, context) { /* ... */ },
renderResult(result, options, theme, context) { /* ... */ },
});
```
Truncate large output:
```typescript
import {
truncateHead, truncateTail, truncateLine,
DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES,
} from "@mariozechner/pi-coding-agent";
const truncation = truncateHead(output, {
maxLines: DEFAULT_MAX_LINES,
maxBytes: DEFAULT_MAX_BYTES,
});
```
### Persisting State
`pi.appendEntry("my-state", { count: 42 })` writes to the session log without entering LLM context. Restore on `session_start` by iterating `ctx.sessionManager.getEntries()` and matching `entry.customType`.
### Registering Commands, Shortcuts, Flags
```typescript
pi.registerCommand("stats", {
description: "Show stats",
getArgumentCompletions: (prefix) => [{ value: "dev", label: "dev" }],
handler: async (args, ctx) => { ctx.ui.notify(`Args: ${args}`, "info"); },
});
pi.registerShortcut("ctrl+shift+p", {
description: "Toggle plan mode",
handler: async (ctx) => { /* ... */ },
});
pi.registerFlag("plan", { description: "Start in plan mode", type: "boolean", default: false });
if (pi.getFlag("plan")) { /* ... */ }
```
Multiple extensions registering the same command name get numeric suffixes (`/review:1`, `/review:2`).
### Registering a Provider
```typescript
pi.registerProvider("my-proxy", {
name: "My Proxy",
baseUrl: "https://proxy.example.com",
apiKey: "PROXY_API_KEY",
api: "anthropic-messages",
models: [{
id: "claude-sonnet",
name: "Claude Sonnet",
reasoning: false,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 16384,
}],
});
```
You can also override built-in providers (e.g. `pi.registerProvider("anthropic", { baseUrl: "..." })`) and `pi.unregisterProvider(name)` to restore defaults.
## ExtensionContext Surface
UI: `ctx.ui.select / confirm / input / editor / notify / setStatus / setWidget / setTitle / setEditorText / pasteToEditor / setTheme / getAllThemes`.
Session: `ctx.sessionManager.getEntries() / getBranch() / getLeafId() / getSessionFile() / getLabel(id)`.
State: `ctx.cwd`, `ctx.hasUI`, `ctx.signal`, `ctx.isIdle()`, `ctx.abort()`, `ctx.hasPendingMessages()`, `ctx.shutdown()`, `ctx.getContextUsage()`, `ctx.compact()`, `ctx.getSystemPrompt()`.
Models: `ctx.modelRegistry.find("anthropic", "claude-sonnet")`, `ctx.model`.
Command handlers receive `ExtensionCommandContext` with extra session-control: `waitForIdle()`, `newSession({ parentSession, setup, withSession })`, `fork(entryId, { position: "before" })`, `navigateTree(entryId, { summarize: true })`, `switchSession(path)`, `reload()`. Inside `withSession`, the passed ctx is bound to the new session — old captured objects are stale.
## Key Examples
The pi docs reference working samples in `examples/extensions/`: `quickstart.ts`, `summarize.ts`, `snake.ts`, `send-user-message.ts`, `input-transform.ts`, `tool-override.ts`, `dynamic-tools.ts`, `truncated-tool.ts`, `github-issue-autocomplete.ts`, `ssh.ts`, `structured-output.ts`, `timed-confirm.ts`.
For overriding built-ins (e.g. `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`), register a tool with the same name. Rendering inheritance is per-slot — omit `renderCall` to keep the built-in's renderer. Disable all built-ins with `--no-builtin-tools`.
For multi-process safety on file mutations:
```typescript
import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
import { resolve } from "node:path";
async execute(_id, params, _signal, _onUpdate, ctx) {
const abs = resolve(ctx.cwd, params.path);
return withFileMutationQueue(abs, async () => {
// read, modify, write
return { content: [/* ... */], details: {} };
});
}
```
## Common Pitfalls
- **Returning error indicators instead of throwing.** Tools must `throw new Error(...)` to signal failures; pi sets `isError: true` automatically.
- **Forgetting to truncate.** Large stdout drowns the LLM context — always use `truncateHead/truncateTail/truncateLine`.
- **Holding stale context inside `withSession`.** The injected ctx targets the replacement session; the outer ctx is no longer current.
- **Trusting the wrong source.** Extensions execute arbitrary code; review before installing.
- **Recursive `prompts/` discovery.** Pi searches `prompts/` non-recursively — same expectation applies to extensions discovered by convention.
- **Skipping `prepareArguments`.** Use it as a compat shim when renaming tool params (translate `oldAction` to `action`).
- **Blocking `tool_call` without a `reason`.** UI surfaces the reason — leaving it empty makes the block opaque.
## When to Use This Mode
- Writing a new pi extension or migrating one between versions
- Adding lifecycle hooks (`tool_call` blocking, `before_agent_start` context injection, `input` interception)
- Registering a custom tool, slash command, shortcut, CLI flag, or model provider
- Building TUI affordances (widgets, status, autocompletes, custom components, themes)
- Overriding built-in tools or routing them to remote backends (SSH, containers)
- Diagnosing event ordering or stale-context bugs
## Sources
- https://pi.dev/docs/latest/extensions
- https://github.com/badlogic/pi-skills
- https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!