Extensions โ€” Rewire the Harness in TypeScript

Lesson 4: Extensions โ€” Rewire the Harness in TypeScript

This is pi's primary extension surface. An extension is a TypeScript module that exports a default factory function receiving the ExtensionAPI. It can subscribe to lifecycle events, register tools the LLM can call, add slash commands, shortcuts, and CLI flags, render custom TUI components, and persist state across restarts. Extensions are loaded with jiti, so TypeScript runs without a build step, and they hot-reload with /reload.

Security: Extensions run with your full system permissions and can execute arbitrary code. Only install and run extensions from sources you trust.

Locations

~/.pi/agent/extensions/*.ts        # global (all projects)
~/.pi/agent/extensions/*/index.ts  # global, multi-file
.pi/extensions/*.ts                # project-local (after project trust)
.pi/extensions/*/index.ts          # project-local, multi-file
# quick tests:
pi -e ./my-extension.ts

A minimal extension

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

export default function (pi: ExtensionAPI) {
  // Block dangerous tool calls before they run
  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" };
    }
  });

  // 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) {
      return { content: [{ type: "text", text: `Hello, ${params.name}!` }], details: {} };
    },
  });

  // A slash command
  pi.registerCommand("hello", {
    description: "Say hello",
    handler: async (args, ctx) => { ctx.ui.notify(`Hello ${args || "world"}!`, "info"); },
  });
}

What you can hook

SurfaceAPIWhat it's for
Lifecycle eventspi.on("session_start" | "project_trust" | "resources_discover" ...)Init work, trust decisions, resource loading
Agent eventsbefore_agent_request and friendsInspect/modify what gets sent to the model
Tool eventstool_call, tool result eventsBlock, allow, or rewrite tool calls (permission gates)
Input eventsinputIntercept, transform, or handle user prompts
Model eventsmodel switch / thinking eventsReact to model and reasoning changes
Commandspi.registerCommand()Custom slash commands (/mycommand)
Shortcuts / flagspi.registerShortcut(), pi.registerFlag()Keybindings and CLI flags
Custom toolspi.registerTool()New tools the LLM can call (typebox schemas)
UIctx.ui โ€” notify, confirm, select, input, setStatus, setWidget, custom()Dialogs, wizards, footer status, widgets, full TUI components
Statepi.appendEntry(), setLabel(), sendMessage()Persist state that survives restarts; inject messages
RenderingregisterMessageRenderer(), registerEntryRenderer(), registerMarkdownTransformer()Custom display for tool calls, entries, markdown
Runtimepi.exec(), pi.setModel(), pi.setActiveTools(), pi.registerProvider()Run processes, swap models, toggle tool surface, add providers

The extension lifecycle

pi starts โ”œโ”€ project_trust (user/global extensions only) โ”œโ”€ session_start (reason: startup) โ””โ”€ resources_discover user prompt โ”œโ”€ extension commands checked first (bypass if found) โ”œโ”€ input event โ†’ can intercept / transform โ”œโ”€ skill/template expansion โ”œโ”€ before_agent_request โ†’ edit what the model sees โ””โ”€ tool_call events โ†’ block / allow / rewrite extensions can also: ctx.compact() ยท ctx.getContextUsage()

Because the factory can be async, extensions can fetch remote configuration (e.g. dynamically discover models from a local server and register them via pi.registerProvider()) before the session starts. Doc guidance: don't start long-lived background resources from the factory itself โ€” defer them to session_start and clean up in session_shutdown.

Real-world extension patterns

  • Permission gates โ€” confirm before rm -rf, sudo, etc. (the example above)
  • Git checkpointing โ€” stash at each turn, restore on branch
  • Path protection โ€” block writes to .env, node_modules/
  • Custom compaction โ€” summarize the conversation your way (Lesson 7)
  • Interactive tools โ€” questions, wizards, custom dialogs
  • Stateful tools โ€” todo lists, connection pools
  • External integrations โ€” file watchers, webhooks, CI triggers
๐Ÿช™ Token angle: Extensions are also your token-control plane:
  • ctx.compact() โ€” trigger compaction programmatically when you know a milestone has passed.
  • ctx.getContextUsage() โ€” read the current budget and warn/inject reminders near the limit.
  • input event โ€” rewrite verbose user prompts into tighter ones before they reach the model.
  • tool_call blocker โ€” deny expensive tools or cap their output (e.g. refuse bash calls that dump huge files into context).
  • Custom summarizers (via session_before_compact) produce better, shorter summaries โ€” quality in compaction directly reduces what re-enters context later.
  • pi.setThinkingLevel() and pi.setActiveTools() โ€” shrink the tool surface and reasoning budget per task (fewer declared tools = smaller system prompt).
๐Ÿ“บ Watch:

Further Reading

๐Ÿง  Knowledge Check

1. How does pi load TypeScript extensions without a build step?

2. Which event lets an extension block a dangerous tool call like rm -rf before it runs?

3. Which ExtensionContext method lets an extension monitor how much of the context window is in use?