# How to Create Custom Tools for earendil π Agents: A Complete Guide

> Learn how to create custom tools for earendil pi agents. Follow this complete guide to extend agent capabilities with TypeScript and register your tools easily.

- Repository: [Earendil Works/pi](https://github.com/earendil-works/pi)
- Tags: how-to-guide
- Published: 2026-05-25

---

**earendil π agents expose a unified extensions system that lets you add custom tools written in TypeScript by placing an [`index.ts`](https://github.com/earendil-works/pi/blob/main/index.ts) file in `~/.pi/agent/tools/` and registering them via `pi.registerTool()`.**

The earendil-works/pi repository provides a powerful framework for extending agent capabilities beyond built-in utilities like `read` and `bash`. Custom tools allow you to define domain-specific logic, maintain state across conversation turns, and leverage π’s interactive UI helpers to create sophisticated agent workflows.

## Extension Discovery and Loading

When π starts, it automatically discovers custom tools by scanning the `~/.pi/agent/tools/` directory (or any additional paths configured in [`settings.json`](https://github.com/earendil-works/pi/blob/main/settings.json)).

The extension loader, located at [`packages/coding-agent/src/core/extensions/index.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/index.ts), treats every sub-directory containing an [`index.ts`](https://github.com/earendil-works/pi/blob/main/index.ts) file as a **custom-tool module**. This module receives a `pi` API object upon initialization, which you use to register your tool definitions.

## The Tool Registration API

Inside your tool module, you register functionality by calling `pi.registerTool(name, definition)`. The `definition` object adheres to the `ToolDefinition` interface and requires three key properties:

- **`description`** — Human-readable text describing the tool’s purpose to the LLM.
- **`inputSchema`** *(optional)* — A JSON Schema object that validates tool arguments before execution.
- **`run(ctx, args)`** — An async function that implements the tool logic.

The `run` function receives two arguments: the **agent context** (`ctx`) and the parsed arguments (`args`). According to the source code in [`packages/coding-agent/src/core/agent-session.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/agent-session.ts), this context is the same object passed to all extension event handlers, giving your tool access to the full agent environment.

## The Context Object and Execution Flow

The `ctx` object provides essential capabilities for custom tool implementation:

- **Model Registry** — Access LLM configurations via `ctx.modelRegistry`.
- **UI Helpers** — Interact with users through `ctx.ui.confirm()`, `ctx.ui.select()`, `ctx.ui.input()`, and `ctx.ui.notify()` (documented in [`packages/coding-agent/docs/tui.md`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/tui.md)).
- **Session State** — Persist data across turns using `ctx.sessionState`, which maintains state for the duration of the agent session.

When the LLM invokes your tool, π validates arguments against `inputSchema` if provided, executes the `run` function, and returns the result. You can stop further tool calls for the current turn by returning `{ result, terminate: true }`, a feature noted in [`packages/coding-agent/CHANGELOG.md`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/CHANGELOG.md).

## System Prompt Integration

To make your tool visible to the LLM, export a `promptSnippet` string from your module. π injects this text into the "Available tools" section of the system prompt.

If you omit `promptSnippet`, the tool remains callable but hidden from the default prompt—a useful pattern for internal utilities or programmatic tools that should only be invoked explicitly.

## Step-by-Step Implementation

### 1. Create the Tool Directory

Create a new folder under `~/.pi/agent/tools/`, for example `markdown-summary/`, and add an [`index.ts`](https://github.com/earendil-works/pi/blob/main/index.ts) entry file.

### 2. Define the Tool Interface

Import the registration types and define your tool's metadata. The example below follows the pattern shown in [`packages/coding-agent/examples/extensions/hello.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/hello.ts):

```typescript
// ~/.pi/agent/tools/markdown-summary/index.ts
import { ToolDefinition, ToolResult } from '@mariozechner/pi-coding-agent';

export const promptSnippet = '- `markdown_summary` – produce a concise summary of a markdown file.';

export const definition: ToolDefinition = {
  description: 'Summarize a markdown document.',
  inputSchema: {
    type: 'object',
    properties: {
      filePath: { 
        type: 'string', 
        description: 'Absolute path to the markdown file' 
      },
    },
    required: ['filePath'],
  },

  async run(ctx, { filePath }) {
    const ok = await ctx.ui.confirm({
      message: `Summarize ${filePath}?`,
      default: true,
    });
    
    if (!ok) return { result: 'Cancelled by user' };

    const content = await ctx.fs.readFile(filePath, 'utf-8');
    const lines = content.split('\n').filter(l => l.trim() !== '');
    const summary = lines.slice(0, 5).join('\n');
    
    ctx.sessionState.lastSummarised = filePath;
    
    return { result: summary };
  },
};

export default (pi) => {
  pi.registerTool('markdown_summary', definition);
};

```

### 3. Register via the Default Export

The module must export a default function that accepts the `pi` object and calls `registerTool()`. This registration hook is invoked by the loader in [`packages/coding-agent/src/core/extensions/index.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/index.ts) during agent startup.

### 4. Invoke the Tool

Once registered, invoke the tool via CLI flags (documented in [`packages/coding-agent/docs/usage.md`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/usage.md)):

```bash
pi -t markdown_summary "Summarize the README"

```

Or reference it by name within prompts:

```

Please use markdown_summary on ./docs/architecture.md

```

## Key Source Files Reference

Understanding the following files helps when debugging or extending tool behavior:

- **[`packages/coding-agent/docs/extensions.md`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md)** — Documents the extensions system, registration API, and `promptSnippet` handling.
- **[`packages/coding-agent/docs/tui.md`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/tui.md)** — Describes UI helpers available via `ctx.ui`.
- **[`packages/coding-agent/examples/extensions/hello.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/hello.ts)** — Minimal working example of a custom tool.
- **[`packages/coding-agent/examples/sdk/06-extensions.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/sdk/06-extensions.ts)** — Demonstrates SDK-based tool registration outside the extensions directory.
- **[`packages/coding-agent/src/core/extensions/index.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/index.ts)** — Core loader that discovers and initializes tool modules.
- **[`packages/coding-agent/src/core/agent-session.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/agent-session.ts)** — Handles tool invocation, context passing, and the `tool_result` hook emissions.
- **[`packages/coding-agent/src/core/sdk.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/sdk.ts)** — Exposes `createAgentSession` where custom tools can be passed via the `tools` option for programmatic use.

## Summary

- Custom tools reside in `~/.pi/agent/tools/` with an [`index.ts`](https://github.com/earendil-works/pi/blob/main/index.ts) entry point.
- Register tools using `pi.registerTool(name, definition)` with a `description`, optional `inputSchema`, and `run` function.
- Access UI helpers, model registry, and session state through the `ctx` object passed to `run()`.
- Export a `promptSnippet` to expose the tool in the system prompt; omit it for hidden tools.
- Return `{ result, terminate: true }` to prevent additional tool calls in the current turn.

## Frequently Asked Questions

### Where should I place files for custom tools in earendil π?

Place each custom tool in its own sub-directory under `~/.pi/agent/tools/`. The directory name becomes your tool's namespace, and the [`index.ts`](https://github.com/earendil-works/pi/blob/main/index.ts) file inside serves as the entry point that the loader in [`packages/coding-agent/src/core/extensions/index.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/index.ts) automatically discovers on startup.

### How can custom tools persist data across conversation turns?

Use `ctx.sessionState`, a session-wide storage object provided to the `run()` function. According to the implementation in [`packages/coding-agent/src/core/agent-session.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/agent-session.ts), any data attached to this object remains available for the duration of the agent session, allowing you to track context or cache results between LLM turns.

### Can I validate arguments before my custom tool executes?

Yes. Provide an `inputSchema` object in your `ToolDefinition` following JSON Schema syntax. The pi agent validates incoming arguments against this schema before invoking your `run()` function, preventing malformed data from reaching your implementation logic.

### How do I make a custom tool available only programmatically but not to the LLM directly?

Omit the `promptSnippet` export from your tool module. As documented in [`packages/coding-agent/docs/extensions.md`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md), tools without a prompt snippet remain callable via the SDK or internal hooks but are excluded from the system prompt's "Available tools" list, effectively hiding them from the LLM's default tool selection.