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
+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 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.