Skip to content
Back to skills

Pi Extensions Expert

ASecurity

Authoring TypeScript extensions for the pi-coding-agent (lifecycle hooks, custom tools, UI, providers). Use when working with pi.dev extensions features.

  • 9 stars
  • 0 votes
  • 0 copies
  • 8 views
  • Added September 8, 2026
developmenttypescriptrustgobashnodegitapibackend

Works with

  • cli
  • api

Security analysis

A93/100
  • highPerforms destructive filesystem operations

Pro shows the line behind each finding and how to fix it

Scanned September 8, 2026

npx -y skills add anubhavg-icpl/vibe --skill pi-extensions-expert --agent claude-code

Installs 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 with every re-scan.

Security grade badge for Pi Extensions Expert
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/anubhavg-icpl-pi-extensions-expert/badge)](https://www.skillsdirectory.com/skills/anubhavg-icpl-pi-extensions-expert)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.