guides / building-extensions

Building extensions

TypeScript modules with event hooks, custom tools, slash commands and TUI widgets.

The mental model

A pi extension is a single TypeScript (or JavaScript) module that exports a default function. pi calls it once at startup with an ExtensionAPI object, and from there you can hook lifecycle events, register tools the LLM can call, add slash commands and keyboard shortcuts, draw TUI widgets, and even rewrite or block tool calls before they run.

No build step. Drop a .ts file in the right directory and /reload.

Hello, extension

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

export default function (pi: ExtensionAPI) {
  // React to lifecycle events
  pi.on("session_start", async (_event, ctx) => {
    ctx.ui.notify("Extension loaded!", "info");
  });

  // Give the model a new tool
  pi.registerTool({
    name: "greet",
    description: "Greet someone",
    parameters: Type.Object({ name: Type.String() }),
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      return { content: [{ type: "text", text: `Hello, ${params.name}!` }] };
    },
  });

  // Add a slash command for the human
  pi.registerCommand("hello", {
    handler: async (args, ctx) => {
      ctx.ui.notify(`Hello ${args || "world"}!`, "info");
    },
  });
}

Try it instantly

pi -e ./my-extension.ts

For permanent use, put it in ~/.pi/agent/extensions/ (global) or .pi/extensions/ (per project). Both accept single files or folders with an index.ts. Auto-discovered extensions hot-reload with /reload.

What the API gives you

  • Events — subscribe with pi.on(...) to hooks like session_start, before_agent_start, input, tool_call and model_select to observe or modify behavior at each step.
  • Toolspi.registerTool() adds model-callable tools with typed parameters, streaming updates and custom rendering. You can also override or wrap the built-in tools — that’s how sandboxing and permission extensions work.
  • Commands & shortcutspi.registerCommand("mycommand", ...) adds /mycommand; keyboard shortcuts like ctrl+x are supported too.
  • UIctx.ui offers dialogs (select, confirm, input), notifications, status indicators and persistent custom widgets.
  • Messages — inject context for the model with pi.sendMessage() or simulate user input with pi.sendUserMessage().
  • State — persist entries into the session with pi.appendEntry() and rebuild your state on session_start, so your extension survives restarts and /reload.

Ground rules worth knowing

  • Truncate tool output. pi’s built-in tools cap output around 50KB / 2,000 lines to protect the context window — yours should too.
  • Tools run in parallel by default. If your tool mutates files, serialize with the provided file-mutation queue helper to avoid lost writes.
  • Sessions can be replaced (e.g. after await ctx.reload()); don’t reuse stale context objects across that boundary.

Study working code

The fastest way to learn is reading small real extensions:

When it works, package it and publish.

📖 This is a community quickstart. The canonical reference is the official pi documentation — always trust it over us if the two disagree.