Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.dari.dev/llms.txt

Use this file to discover all available pages before exploring further.

Pi extensions are TypeScript or JavaScript modules that customize the Pi harness. Use them when you want code to run inside the agent loop itself: intercept tool calls, register Pi-native tools, add runtime checks, transform prompts, persist extension state, or react to session lifecycle events. Use the Pi extension docs and example extensions to learn how to write an extension. This page covers how to bundle one with a Dari agent. A Dari agent using harness: pi can bundle Pi extensions alongside its manifest. Put each extension under extensions/<name>/, declare it in dari.yml, then deploy normally.
my-agent/
  dari.yml
  prompts/system.md
  extensions/permission-gate/
    index.ts
# dari.yml
name: guarded-agent
harness: pi

instructions:
  system: prompts/system.md

llm:
  model: openai/gpt-5.5

extensions:
  - name: permission-gate
    path: extensions/permission-gate
// extensions/permission-gate/index.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.on("tool_call", async (event, ctx) => {
    if (event.toolName !== "bash") return;

    const command = event.input.command as string;
    if (!/\brm\s+(-rf?|--recursive)\b/i.test(command)) return;

    if (!ctx.hasUI) {
      return { block: true, reason: "Blocked dangerous command" };
    }

    const allow = await ctx.ui.confirm(
      "Dangerous command",
      `Allow this command?\n\n${command}`,
    );

    if (!allow) return { block: true, reason: "Blocked by user" };
  });
}
Deploy the agent after adding the extension:
dari deploy .

When to use an extension

Use a Pi extension when the behavior needs Pi runtime hooks. Common examples include blocking or rewriting tool calls, adding per-turn context, registering a tool with pi.registerTool(), changing the active tool set, responding to model or session events, and storing extension-specific state in the session. Use a Dari custom_tools: entry instead when you only need a declared tool with a JSON schema, especially if your application will execute the tool externally and submit the result back through the API.

Folder shapes

Each declared path must be exactly extensions/<name>. The folder must contain one of the Pi-discoverable entry points below.
extensions/guardrails/index.ts
extensions/guardrails/index.js
extensions/guardrails/package.json   # with a non-empty pi.extensions array
For an extension with npm dependencies, include a package.json in the extension folder. If you include package-lock.json, Dari installs with npm ci; otherwise it installs with npm install.
{
  "name": "guardrails-extension",
  "private": true,
  "type": "module",
  "pi": {
    "extensions": ["./index.ts"]
  },
  "dependencies": {
    "ms": "^2.1.3"
  }
}

Runtime behavior

Extensions run in the session sandbox, not on the machine that ran dari deploy. They see the bundled project as the session workspace and can use the same Pi extension APIs they would use locally. Because Dari sessions are driven through an API rather than the local Pi TUI, do not assume interactive UI is always available. Check ctx.hasUI before calling blocking UI methods such as ctx.ui.confirm(), and provide a safe non-interactive path for API sessions.
Dari uses the top-level extensions/ directory in your agent bundle. Do not create or rely on .pi/extensions inside a Dari agent project.

Validation rules

Dari validates extensions at publish time:
  • If an extensions/ directory exists, dari.yml must include an extensions: list.
  • Every immediate child folder under extensions/ must be declared.
  • Each extension name and path must be unique.
  • path must be exactly extensions/<name>.
  • Nested extension paths are not supported.
  • The declared folder must contain index.ts, index.js, or a package.json with a non-empty pi.extensions array.
See the Manifest Reference for the exact dari.yml schema.