pi-map_
← ~/guides

Extensions 101

Last updated: Jul 27, 2026

$ pi –extend

Pi’s design keeps the core small and pushes workflow-specific behavior into extensions. An extension is not a plugin in a sandboxed marketplace sense — it is a TypeScript module that runs inside the Pi process with your full system permissions, able to subscribe to lifecycle events, register tools the LLM can call, add slash commands, render custom UI, and intercept tool calls. This guide covers the real mechanism, as documented in the repo’s docs/extensions.md.

What an extension is

An extension is a .ts file whose default export is a factory function that receives an ExtensionAPI object (conventionally named pi). Pi loads extensions with jiti, so TypeScript runs without a compile step:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  // subscribe to events, register tools/commands/shortcuts/flags
}

The factory may be async; Pi awaits it before continuing startup (before session_start and before provider registrations are flushed). Don’t start long-lived background resources in the factory — defer those to session_start and close them in a session_shutdown handler.

Where extensions live

Auto-discovered locations (these hot-reload with /reload):

Location Scope
~/.pi/agent/extensions/*.ts Global
~/.pi/agent/extensions/*/index.ts Global (directory form)
.pi/extensions/*.ts Project-local
.pi/extensions/*/index.ts Project-local (directory form)

Project-local extensions load only after the project is trusted (see the trust prompt or /trust). For quick tests, use pi -e ./path.ts — repeatable, and combinable with --no-extensions to isolate:

pi --no-extensions -e ./my-extension.ts

Extensions can also be shared as pi packages from npm or git (pi install <source>), and extra paths can be listed under packages / extensions in settings.json. If your extension needs npm dependencies, drop a package.json next to it, run npm install, and imports from node_modules/ resolve automatically.

Security: extensions execute arbitrary code with your permissions. Only install from sources you trust.

What extensions can do

  • pi.on(event, handler) — subscribe to lifecycle events; many handlers can block, modify, or inject
  • pi.registerTool({...}) — add a tool the LLM can call (registering the same name as a built-in overrides it)
  • pi.registerCommand("name", {...}) — add /name slash commands
  • pi.registerShortcut(...), pi.registerFlag(...) — keybindings and CLI flags
  • pi.registerProvider(...) — add a custom model provider
  • pi.appendEntry(...) — persist state into the session file
  • ctx.uiselect, confirm, input, notify, custom() full TUI components, setStatus, setWidget
  • Custom renderers for tool calls/results and messages

The event lifecycle

Events mirror the agent loop. The core sequence:

pi starts
  ├─► project_trust          (user/global extensions only)
  ├─► session_start          { reason: "startup" }
  └─► resources_discover
user sends prompt
  ├─► input                  (can intercept/transform)
  ├─► before_agent_start     (can inject message, modify system prompt)
  ├─► agent_start
  │     per turn:
  │       turn_start → context (can modify messages)
  │       → before_provider_request → after_provider_response
  │       → tool_execution_start → tool_call (can block)
  │       → tool_result (can modify) → tool_execution_end → turn_end
  ├─► agent_end
  └─► agent_settled

Session operations fire their own events: /new and /resumesession_before_switch, session_shutdown, session_start; /compactsession_before_compact, session_compact; model changes → model_select, thinking_level_select; exit → session_shutdown.

A minimal working example

Adapted from the official quick start. Save as ~/.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");
  });

  // Intercept dangerous tool calls
  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 the LLM can call
  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 /hello command
  pi.registerCommand("hello", {
    description: "Say hello",
    handler: async (args, ctx) => {
      ctx.ui.notify(`Hello ${args || "world"}!`, "info");
    },
  });
}

Restart Pi (or /reload), then: the model can now call a greet tool, /hello world works, and any rm -rf attempt triggers a confirmation dialog.

Available imports in extensions: @earendil-works/pi-coding-agent (types), typebox (parameter schemas), @earendil-works/pi-ai, @earendil-works/pi-tui (custom UI), Node.js built-ins, and anything in a local node_modules/.

Real examples to study

The repo ships dozens of working extensions in packages/coding-agent/examples/extensions/. Good starting points:

  • hello.ts — minimal tool registration
  • confirm-destructive.ts — permission gate pattern
  • git-checkpoint.ts — stash per turn, restore on branch
  • question.ts / questionnaire.ts — interactive tools via ui.select / ui.custom
  • todo.ts — stateful tool with session persistence
  • custom-compaction.ts — replace the compaction summarizer
  • summarize.ts — conversation summary command
  • snake.ts — a game in the TUI, because why not

The fastest way to write one, per the official docs: pi can create extensions — just ask it to build one for your use case.

Further reading