← Pi Agents

The Built-In Tools

The eight built-in Pi tools, what defaultTools actually selects, how tool results reach the transcript, and when to add codemode.

Pi shows every tool call and result while it works. That is a promise about visibility, not about restriction: it does not ask before every tool call, and the tools run with the operating-system permissions of the process.

What you can control is the set. settings.md defaults defaultTools to read, bash, edit, and write. Every run starts from that list unless you change it.

The eight

cli.md lists the built-in tools and their purpose:

Tool Purpose
read Read text files and supported images
bash Run shell commands
powershell Run PowerShell commands on Windows
edit Apply exact text replacements to an existing file
write Create or overwrite a file
grep Search file contents
find Find paths using glob patterns
ls List directory contents

Two more come from built-in extensions and are off by default: codemode, which runs JavaScript that calls the other tools, and tool_search, which searches tools not declared to the model. settings.md notes the MCP extension turns them on when a server needs them — codemode for servers with codemode exposure, tool_search for servers with deferred exposure.

How the default list is composed

defaultTools is subtler than it looks. Plain names replace the defaults; +name adds; -name removes. And the merge is not symmetric — project settings apply on top of user settings:

  • A project list of only +name and -name entries changes the user’s selection.
  • A project list containing a plain name replaces it.
  • Within one list, plain names form the selection, and +name and -name then apply in order.

So ["+grep", "+find", "+ls"] in a project settings file adds three search tools on top of whatever the user has. ["read", "powershell", "edit", "write"] replaces the selection outright.

Four built-in extensions are named builtin:mcp, builtin:llama.cpp, builtin:codemode and builtin:tool-search in extensions. They load by default; -builtin:mcp disables one.

One-shot overrides

--tools replaces the whole selection, so name every tool you want. This is the one place where +name and -name are rejected:

pi --tools read,grep,find,ls --print "Review this project"
pi --tools read,bash,edit,write,codemode

The other three flags, in increasing severity: -xt/--exclude-tools disables names after all other selection options; -nbt/--no-builtin-tools keeps extension and custom tools; -nt/--no-tools starts with everything disabled.

The reload trap

/reload enables tools newly added to defaultTools. It does not disable tools removed from it, and it does not re-enable unchanged tools you turned off. --tools, --no-tools, and --no-builtin-tools override the setting for one invocation, including on reload.

If you disable a tool and it stays disabled after editing settings.json, that is documented behaviour, not a bug. Restart Pi.

What a tool result looks like

message-types.md defines ToolResultMessage:

interface ToolResultMessage<TDetails = any> {
  role: "toolResult";
  toolCallId: string;
  toolName: string;
  content: (TextContent | ImageContent)[];
  details?: TDetails;
  usage?: Usage;
  isError: boolean;
  timestamp: number;
}

Three things to carry into the extension chapters. details is tool-specific structured data. Optional usage reports nested model work performed by the tool — it contributes to full-session statistics but is not part of the main model-call usage. And isError is an ordinary field.

Returning is not failing

extensions.md states the rule bluntly: throw from execute() to produce a failed tool result, and returning an object does not mark it as an error.

That covers the built-ins too. codemode.md says a bash call resolves to a structured value “also for non-zero exit codes”:

const r = await tools.bash({ command: "npm test" });
if (r.exit_code !== 0) text(`tests failed: ${r.output}`);

A non-zero exit is data. A throw is failure. Conflating them makes a model treat a red test run as a broken tool and retry the tool instead of fixing the code.

codemode changes the shape of a turn

codemode is a built-in extension that registers a tool taking raw JavaScript. The script runs as the body of an async function in a QuickJS sandbox, so top-level await and return work. It has no Node APIs, file system, network, or timers; it reaches the outside world only through tools and models.

Only the script’s output reaches the model. That is the whole point:

// @options: {"max_output_tokens": 2000, "timeout_ms": 60000}
const results = await Promise.allSettled([
  tools.bash({ command: "npm test -- --run tests/auth" }),
  tools.bash({ command: "npm test -- --run tests/billing" }),
  tools.bash({ command: "npm run lint" }),
]);

for (const [i, r] of results.entries()) {
  text(r.status === "fulfilled" ? `job ${i}: exit ${r.value.exit_code}` : `job ${i} failed: ${r.reason}`);
}

Three tool calls, one model-visible message. The sandbox has a 256 MB memory limit; filter and aggregate rather than accumulating.

While codemode is active, codemode.mode decides how the other tools are presented. With on (the default) declared tools stay declared and their descriptions say how to call them from scripts. With only they are hidden from the model and listed in the codemode description instead.

Declaring tools is not activating tools

From extensions.md: the active set, read through pi.getActiveTools() and written through pi.setActiveTools(), is the set of tools declared to the model. pi.getAllTools() reports every registered tool with its exposure, namespace and annotations. There are two lists, and they are not the same list.

Wrong: “I registered the tool, so the model can see it.”

Correct: “I registered the tool, so it exists. Whether the model can see it is a separate question with a separate answer.”

The documentation is precise about where the two differ: registering a direct or model-only tool activates it, and the other exposures do not activate on registration. codemode and deferred are the ones that surprise people, which is why the built-in extensions ship them inactive. Chapter 17 works through all five exposures.

Prefer search to guessing

Our recommendation, not Pi’s: add the search tools before anything else.

{
  "defaultTools": ["+grep", "+find", "+ls"]
}

In our experience a model without them reaches for bash with cat or ls piped into shell logic instead, which costs a full process launch and puts the output through the shell rather than through a structured result. If you see that happening, these three entries are the fix.

Next

defaultTools is one setting, and it composes with the others in ways chapter 6 has only hinted at: a project file with a plain name replaces the user’s list, one with only + and - entries amends it. That asymmetry is not a curiosity, it is the rule that governs every setting in Pi, so the next chapter maps the whole configuration surface and the precedence between the layers before any of it is written down for a specific tool.

Chapter 7: settings and where configuration comes from.