# Extensions and custom tools

> Register extensions, custom tools, plan-mode and subagent extension samples, allowlists, and dynamic resource patterns.

- Repository: PrimeIntellect-ai/prime-agent
- GitHub: https://github.com/PrimeIntellect-ai/prime-agent
- Human docs: https://grok-wiki.com/public/docs/primeintellect-ai-prime-agent-3367c32760b1
- Complete Markdown: https://grok-wiki.com/public/docs/primeintellect-ai-prime-agent-3367c32760b1/llms-full.txt

## Source Files

- `packages/coding-agent/examples/extensions/README.md`
- `packages/coding-agent/examples/sdk/06-extensions.ts`
- `packages/coding-agent/examples/sdk/05-tools.ts`
- `packages/coding-agent/examples/extensions/plan-mode/README.md`
- `packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts`
- `packages/coding-agent/examples/extensions/dynamic-resources/SKILL.md`

---

---
title: "Extensions and custom tools"
description: "Register extensions, custom tools, plan-mode and subagent extension samples, allowlists, and dynamic resource patterns."
---

Extensions are TypeScript modules that receive an `ExtensionAPI` (`pi`) and can intercept agent events, register tools and commands, and extend UI and resources. Prime Agent loads them from the CLI (`--extension` or an auto-discovery directory), from project/agent discovery paths used by the SDK resource loader, or from inline `extensionFactories` on `DefaultResourceLoader`.

## Load an extension

### CLI

From the repository root:

```bash
./prime-agent.sh --extension packages/coding-agent/examples/extensions/permission-gate.ts
```

Copy an example into the agent extensions directory for auto-discovery:

```bash
cp permission-gate.ts ~/.prime/agent/extensions/
```

Example extensions live under `packages/coding-agent/examples/extensions/`.

### SDK discovery paths

`createAgentSession` with `DefaultResourceLoader` discovers extension files from:

| Source | Path / key |
|--------|------------|
| Agent dir | `~/.pi/agent/extensions/` |
| Project cwd | `<cwd>/.pi/extensions/` |
| Settings | `settings.json` `"extensions"` array |
| Loader option | `additionalExtensionPaths: string[]` |
| Loader option | `extensionFactories: ((pi) => void)[]` |

<Note>
CLI example docs use `~/.prime/agent/extensions/` for copy-based auto-discovery. SDK comments document `~/.pi/agent/extensions/` and `<cwd>/.pi/extensions/`. Use the path your entrypoint documents for that surface.
</Note>

### Loader and session bind

```ts
import {
  createAgentSession,
  DefaultResourceLoader,
  getAgentDir,
  SessionManager,
} from "@earendil-works/pi-coding-agent";

const resourceLoader = new DefaultResourceLoader({
  cwd: process.cwd(),
  agentDir: getAgentDir(),
  additionalExtensionPaths: ["./my-logging-extension.ts", "./my-safety-extension.ts"],
  extensionFactories: [
    (pi) => {
      pi.on("agent_start", () => {
        console.log("[Inline Extension] Agent starting");
      });
    },
  ],
});
await resourceLoader.reload();

const { session } = await createAgentSession({
  resourceLoader,
  sessionManager: SessionManager.inMemory(),
});
```

Regression coverage also calls `await session.bindExtensions({})` after `createAgentSession` so factory-registered tools attach to the session.

## Extension module contract

An extension file exports a default function:

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

export default function (pi: ExtensionAPI) {
  // hooks, tools, commands
}
```

Documented hook and registration surfaces from the SDK sample:

| Surface | Role |
|---------|------|
| `pi.on("agent_start", …)` | Agent start |
| `pi.on("tool_call", …)` | Observe or block a tool call |
| `pi.on("agent_end", …)` | End of agent turn; receives `event.messages` |
| `pi.on("session_start", …)` | Session start (used for dynamic tool registration) |
| `pi.registerTool({ … })` | Custom tool |
| `pi.registerCommand(name, { description, handler })` | Slash/command handler |

`tool_call` may return `{ block: true, reason: "..." }` to block execution, or `undefined` to allow.

## Register a custom tool

Custom tools are not passed as ad-hoc objects on `createAgentSession`. Register them through the extensions system with `pi.registerTool()`.

```ts
pi.registerTool({
  name: "my_tool",
  label: "My Tool",
  description: "Does something useful",
  parameters: Type.Object({
    input: Type.String(),
  }),
  execute: async (_toolCallId, params, _signal, _onUpdate, _ctx) => ({
    content: [{ type: "text", text: `Processed: ${params.input}` }],
    details: {},
  }),
});
```

### Tool definition fields (sample)

| Field | Purpose |
|-------|---------|
| `name` | Tool id used by allowlists and activation |
| `label` | Display label |
| `description` | Tool description |
| `parameters` | TypeBox schema (`Type.Object`, …) |
| `promptSnippet` | Optional prompt text (used in dynamic/regression samples) |
| `execute` | Async runner; returns `{ content, details }` |

`content` items use `{ type: "text", text: string }`.

### Register a command

```ts
pi.registerCommand("mycommand", {
  description: "Do something",
  handler: async (args, ctx) => {
    ctx.ui.notify(`Command executed with: ${args}`);
  },
});
```

## Tool allowlists

`createAgentSession({ tools: string[] })` is an allowlist over **built-in, extension, and custom** tool names. Names match whatever tools are available after extensions bind.

```ts
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";

await createAgentSession({
  tools: ["ipython"],
  sessionManager: SessionManager.inMemory(),
});
```

With a custom project root, pass the same `cwd` into the session and the in-memory session manager so built-ins resolve against that directory:

```ts
const customCwd = "/path/to/project";
await createAgentSession({
  cwd: customCwd,
  tools: ["ipython"],
  sessionManager: SessionManager.inMemory(customCwd),
});
```

### Verified allowlist behavior

From regression `#2835` (`tools` + extension `session_start` registration of `dynamic_tool`):

| `tools` value | Active tools | System prompt notes |
|---------------|--------------|---------------------|
| `["ipython", "dynamic_tool"]` | Only `ipython` and `dynamic_tool` (`getAllTools` / `getActiveToolNames`) | Non-listed tools (e.g. `bash`, `edit`) do not appear as tool bullets |
| `[]` | None | No `Available tools:` section; dynamic tool name absent |

<Warning>
An empty allowlist disables **all** tools, including extension tools registered on `session_start`. List both built-in and extension tool names you need.
</Warning>

## Dynamic tools and resources

### Register after startup

`dynamic-tools.ts` (catalog) and the regression factory register tools on `session_start`:

```ts
pi.on("session_start", () => {
  pi.registerTool({
    name: "dynamic_tool",
    label: "Dynamic Tool",
    description: "Tool registered from session_start",
    promptSnippet: "Run dynamic test behavior",
    parameters: Type.Object({}),
    execute: async () => ({
      content: [{ type: "text", text: "ok" }],
      details: {},
    }),
  });
});
```

Catalog notes for `dynamic-tools.ts`: register tools after startup (`session_start`) and at runtime via command, with prompt snippets and tool-specific prompt guidelines.

### `resources_discover`

`dynamic-resources/` loads skills, prompts, and themes via `resources_discover`. Example skill frontmatter shipped with that extension:

```yaml
---
name: dynamic-resources
description: Example skill loaded from resources_discover
---
```

Body: skill content is provided by the dynamic-resources extension.

## Plan-mode sample

`plan-mode/` is a Claude Code-style read-only exploration mode with `/plan` and step tracking.

### Controls

| Input | Effect |
|-------|--------|
| `/plan` | Toggle plan mode |
| `/todos` | Show plan progress |
| `Ctrl+Alt+P` | Toggle plan mode |
| `--plan` | Enable plan mode at launch |

### Workflow

<Steps>
  <Step title="Enable plan mode">
    Use `/plan` or `--plan`.
  </Step>
  <Step title="Explore and draft a plan">
    Ask the agent to analyze code and emit a numbered plan under a `Plan:` header:

    ```
    Plan:
    1. First step description
    2. Second step description
    3. Third step description
    ```
  </Step>
  <Step title="Execute">
    Choose "Execute the plan" when prompted. Full tool access is restored. The agent marks steps with `[DONE:n]`; a progress widget shows completion. State persists across session resume.
  </Step>
</Steps>

### Modes

| Mode | Tools | Behavior |
|------|-------|----------|
| Plan (read-only) | `bash` and `questionnaire` only; bash filtered by allowlist | Plan without edits |
| Execution | Full tool access | Ordered steps, `[DONE:n]`, progress widget |

### Bash allowlist (plan mode)

**Allowed (read-only):** `cat`, `head`, `tail`, `less`, `more`, `grep`, `find`, `rg`, `fd`, `ls`, `pwd`, `tree`, `git status`, `git log`, `git diff`, `git branch`, `npm list`, `npm outdated`, `yarn info`, `uname`, `whoami`, `date`, `uptime`.

**Blocked:** `rm`, `mv`, `cp`, `mkdir`, `touch`, `git add`, `git commit`, `git push`, `npm install`, `yarn add`, `pip install`, `sudo`, `kill`, `reboot`, `vim`, `nano`, `code`.

## Subagent extension sample

Catalog entry `subagent/`: delegate tasks to specialized subagents with isolated context windows. Wire multi-agent messaging and orchestration through the dedicated subagents docs rather than redefining protocol here.

## Extension catalog (examples)

All paths relative to `packages/coding-agent/examples/extensions/`.

### Lifecycle and safety

| Extension | Description |
|-----------|-------------|
| `permission-gate.ts` | Confirm before dangerous bash (`rm -rf`, `sudo`, …) |
| `protected-paths.ts` | Block writes to `.env`, `.git/`, `node_modules/` |
| `confirm-destructive.ts` | Confirm destructive session actions (clear, switch, fork) |
| `dirty-repo-guard.ts` | Block session changes with uncommitted git changes |
| `sandbox/` | OS-level sandboxing via `@anthropic-ai/sandbox-runtime` with per-project config |

### Custom tools

| Extension | Description |
|-----------|-------------|
| `todo.ts` | Todo tool + `/todos`, custom rendering, state persistence |
| `hello.ts` | Minimal custom tool |
| `question.ts` | `ctx.ui.select()` questions |
| `questionnaire.ts` | Multi-question input with tab bar |
| `tool-override.ts` | Override tools (logging/access control) |
| `dynamic-tools.ts` | Tools on `session_start` / command; prompt snippets and guidelines |
| `structured-output.ts` | Final structured-output tool with `terminate: true` |
| `built-in-tool-renderer.ts` | Compact rendering for bash/edit, original behavior |
| `minimal-mode.ts` | Minimal built-in tool rendering (calls only when collapsed) |
| `truncated-tool.ts` | ripgrep wrapper; truncate at 50KB / 2000 lines |
| `ssh.ts` | Remote bash/edit over SSH via pluggable operations |
| `subagent/` | Specialized subagents, isolated context windows |

### Commands and UI (selected)

| Extension | Description |
|-----------|-------------|
| `preset.ts` | Presets for model, thinking, tools, instructions (`--preset`, `/preset`) |
| `plan-mode/` | Read-only plan mode (`/plan`, step tracking) |
| `tools.ts` | Interactive `/tools` enable/disable with session persistence |
| `handoff.ts` | `/handoff <goal>` focused session transfer |
| `send-user-message.ts` | `pi.sendUserMessage()` |
| `rpc-demo.ts` | RPC-supported extension UI methods (pair with `examples/rpc-extension-ui.ts`) |
| `reload-runtime.ts` | `/reload-runtime` and `reload_runtime` tool |
| `shutdown-command.ts` | `/quit` via `ctx.shutdown()` |

Additional UI samples cover status/header/footer widgets, overlays, autocomplete, modal/rainbow editors, desktop notifications (OSC 777), and games (`snake.ts`, `tic-tac-toe.ts` with `executionMode: "sequential"`, `doom-overlay/`).

### System prompt, compaction, resources, messaging

| Extension | Description |
|-----------|-------------|
| `pirate.ts` | `systemPromptAppend` |
| `claude-rules.ts` | Scan `.claude/rules/` into system prompt |
| `custom-compaction.ts` | Custom full-conversation compaction |
| `trigger-compact.ts` | Compact above 100k tokens + `/trigger-compact` |
| `dynamic-resources/` | Skills, prompts, themes via `resources_discover` |
| `message-renderer.ts` | `registerMessageRenderer` |
| `event-bus.ts` | Inter-extension bus via `pi.events` |
| `session-name.ts` | `setSessionName` for session selector |
| `bookmark.ts` | `setLabel` for `/tree` navigation |

### Custom providers (examples)

| Extension | Description |
|-----------|-------------|
| `custom-provider-anthropic/` | Custom Anthropic provider with OAuth and custom streaming |
| `custom-provider-gitlab-duo/` | GitLab Duo via `@earendil-works/pi-ai` streaming API through a proxy |

Providers remain BYOK/BYOC: register your own provider implementation; no single hosted model is required.

## Architecture (registration path)

```mermaid
flowchart TB
  subgraph sources [Extension sources]
    CLI["CLI --extension path"]
    AD["Auto-discovery dirs"]
    SET["settings.json extensions"]
    PATHS["additionalExtensionPaths"]
    FACT["extensionFactories"]
  end

  subgraph loader [DefaultResourceLoader]
    REL["reload()"]
  end

  subgraph session [Agent session]
    CAS["createAgentSession tools allowlist"]
    BIND["bindExtensions"]
    API["ExtensionAPI pi"]
    TOOLS["registerTool / getAllTools"]
    CMDS["registerCommand"]
    HOOKS["on agent_start tool_call session_start agent_end"]
  end

  CLI --> REL
  AD --> REL
  SET --> REL
  PATHS --> REL
  FACT --> REL
  REL --> CAS
  CAS --> BIND
  BIND --> API
  API --> TOOLS
  API --> CMDS
  API --> HOOKS
```

## Verification checklist

| Check | Signal |
|-------|--------|
| CLI load | Extension path accepted via `--extension` or present under auto-discovery dir |
| SDK load | `resourceLoader.reload()` then session runs without loader errors |
| Custom tool visible | Tool name in `session.getAllTools()` / `getActiveToolNames()` after `bindExtensions` |
| Allowlist filter | Only listed names active; `tools: []` → empty tools and no `Available tools:` prompt section |
| Plan mode | `/plan` or `--plan`; tools limited to bash + questionnaire; bash allowlist enforced |
| Dynamic skill | Skill frontmatter `name: dynamic-resources` available when dynamic-resources extension loads via `resources_discover` |

## Related pages

<CardGroup>
  <Card title="Skills, tools, and extensions (SDK)" href="/sdk-skills-tools-extensions">
    SDK recipes for skills, tools, extensions, context files, and subagent extension wiring.
  </Card>
  <Card title="Minimal SDK agent" href="/sdk-minimal">
    Minimal bootstrap, custom prompt/model wiring, first-run expectations.
  </Card>
  <Card title="Skills model" href="/skills-model">
    Skills as packages, SKILL.md constraints, and load-scope precedence.
  </Card>
  <Card title="Create and install skills" href="/create-skills">
    Author skill packages and verify load paths.
  </Card>
  <Card title="Subagents and messaging" href="/subagents-messaging">
    Child agents, agent-message surface, and multi-agent constraints.
  </Card>
  <Card title="Session configuration" href="/session-configuration">
    Session config keys, defaults, and reload behavior.
  </Card>
  <Card title="Built-in skills reference" href="/builtin-skills">
    Shipped skill catalog and entry modules.
  </Card>
  <Card title="Settings and provider keys" href="/settings-providers">
    Provider registration and API key / OAuth wiring for custom providers.
  </Card>
</CardGroup>
