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.
In an interactive session, Pi displays tool activity as it works. Visibility is not restriction: Pi does not require approval before every tool call, and built-in tools ordinarily run with the operating-system permissions of the Pi process. Headless modes expose activity differently, and showing a call does not prevent its effects.
One control is the set of tools made available to the model. settings.md gives defaultTools a default of read, bash, edit, and write. Settings, command-line options and loaded integrations can change the effective set; none of these settings is an operating-system sandbox.
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.
flowchart TD
DEF["defaultTools setting<br/>user, then project on top"] --> SYN["syntax decides the operation:<br/>plain = replace, + = add, - = remove"]
SYN --> BUILT["built-in tools<br/>read bash edit write (+ powershell on Windows)"]
SYN --> EXT["extension and custom tools<br/>registered by extensions"]
SYN --> MCP["MCP tools<br/>kept unless an entry starts with mcp__"]
CLI["--tools / --exclude-tools<br/>one invocation"] -.->|"overrides the setting"| OUT
BUILT --> OUT["the active tool set<br/>= declared to the model"]
EXT --> OUT
MCP --> OUT
SEL["defaultTools: [#quot;+codemode#quot;]"] --> SYN
The diagram separates settings from one-invocation command-line selection. A plain name in defaultTools replaces the ordinary selection rather than adding to it: defaultTools: ["read"] selects read among ordinary tools, not the default four. MCP tools require separate attention, as the command-line section below explains.
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
+nameand-nameentries changes the user’s selection. - A project list containing a plain name replaces it.
- Within one list, plain names form the selection, and
+nameand-namethen 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 ordinary built-in, extension and custom tool selection, so name each of those tools you need. Unlike defaultTools, it does not accept +name or -name modifiers. MCP tool selection has an additional rule described below:
# --no-mcp is here because 1.0.4 keeps MCP tools; see the table below.
pi --tools read,grep,find,ls --no-mcp --print "Review this project"
pi --tools read,bash,edit,write,codemode
Before copying the first line, note that --tools read,grep,find,ls alone is not a complete MCP restriction on 1.0.4: configured MCP tools may remain available when no mcp__ selector is supplied. --no-mcp disables the built-in MCP integration for this run, making the example a narrower tool-selection test. It does not sandbox filesystem or network access through the tools that remain. Chapter 32 applies the same distinction to headless operation.
Three related flags operate differently rather than forming a simple severity ladder: -xt/--exclude-tools excludes matching tools after selection; -nbt/--no-builtin-tools removes built-in tools while leaving other categories eligible; and -nt/--no-tools disables the tool set for the run.
As of 1.0.4, command-line tool selectors accept patterns, and MCP selection has separate semantics. Both --tools and --exclude-tools accept names or anchored patterns where * matches any characters; mcp__radius__*, for example, selects tools under that MCP prefix. Without an mcp__ selector, --tools does not by itself establish that MCP tools are absent. The flags differ as follows:
| Flag | Built-in, extension and custom tools | MCP tools |
|---|---|---|
--tools |
Replaced by the list | Kept, unless an entry starts with mcp__ |
--exclude-tools |
Disabled by the list | Disabled by the list, MCP included |
--no-tools |
All disabled | All disabled |
So pi --tools read,grep,find,ls does not necessarily mean “only these four tools” when MCP servers are configured. Without an explicit MCP selector, MCP tools can remain reachable through their configured exposure. Supplying an mcp__ pattern changes which MCP tools are selected; however, not declared directly to the model is not the same as unreachable through another exposed mechanism, such as codemode or deferred tool search. For an explicit MCP selection, use:
pi --tools read,bash,codemode,'mcp__radius__*'
The MCP resource tools — list_mcp_resources, list_mcp_resource_templates and read_mcp_resource — count as MCP tools for this rule. To drop every one of them use --exclude-tools 'mcp__*', or turn MCP off for the run with --no-mcp, which connects no servers and leaves no MCP tools and no /mcp.
Two distinctions worth holding on to. Patterns are a command-line feature: defaultTools in a settings file takes plain names and the +name/-name modifiers, and a name containing * there is a name that matches no tool. And --no-mcp disables the built-in MCP support for one run; it does not affect an extension that replaces it. Chapter 21 has the exposure-by-exposure table for what “kept” means, including the one case the documentation leaves implicit — a direct-exposure tool that the allowlist does not name stays registered but is neither declared nor callable.
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 = JsonValue> {
role: "toolResult";
toolCallId: string;
toolName: string;
content: (TextContent | ImageContent)[];
details?: TDetails;
usage?: Usage;
nestedCalls?: NestedToolCalls;
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}`);
In this codemode example, a non-zero shell exit is returned as structured data, so the script must inspect exit_code and report it accurately. By contrast, an extension tool that throws from execute() is reported as a failed tool invocation. These are different failure signals; neither guarantees how the model will react.
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.
The purpose of codemode is to let a script aggregate multiple tool results and return a bounded summary to the model, rather than pasting every intermediate result into the main conversation:
// @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.
It also has an output limit, and as of 1.0.1 exceeding it is a failure rather than an accident. A script fails when its output passes 16777216 characters of text and base64 image data, or 100000 calls to text(), image() and console. Both were previously reachable enough to run Pi out of memory, so they are now enforced ceilings.
The lesson is the one the max_output_tokens option above already implies: a script is a place to compute a summary of many calls, not a place to relay them. If the full output matters, the documented advice is to write it to a file with a tool and return the path.
Since 1.0.3, image() can also save an image to a temporary file and expose its path for later use. In 1.0.4, tools.read() can return an image block that a codemode script passes to image(). These conveniences do not change the need to bound script output and review where generated files are stored.
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"]
}
Without dedicated search tools, a model may fall back to shell commands such as cat, find or ls for discovery. Enabling grep, find and ls gives it structured alternatives, but does not guarantee it will choose them or that its searches will be cheaper. Check the actual tool calls before changing the selection.
What was run
The defaultTools rules were run through Pi’s own SettingsManager, over real settings.json files, in examples/ch07-settings/. Nothing re-implements the merge. With a user list of read, bash:
- a project list of only
+grep,+find,+lsgivesread, bash, grep, find, ls; - a project list containing a plain name,
read, powershell, replaces the selection; - within one list, plain names form the selection and
+and-then apply in order:read, bash, -bash, +grepgivesread, grep; - a project list of
-bashalone removesbashfrom the user’s selection.
The one-shot flag was run against the shipped binary in chapter 32’s tests: with --tools read,grep,find,ls, a model that asked for bash got a failed tool result and the command never ran, while the default tool set did run it. The -nt, -nbt and -xt variants and the reload trap were not run.
Next
defaultTools has a particular merge rule: a project list containing a plain name replaces the user’s tool selection, while a list containing only + and - entries amends it. Other settings and resource lists have their own precedence and combination rules. The next chapter maps those differences across Pi’s configuration surfaces.
Chapter 7: settings and where configuration comes from.