Tools — hands with guardrails
The 30-second version: a model's "hands" are tools — small functions it can ask to run. Pi ships four of them (
read,write,edit,bash), wraps every call in validation and hooks, and converts every failure into a message the model can react to. No explosions, ever.
What is a tool, really?
Strip away the ceremony and a tool is three things:
- A name and a description — the part the model sees ("read: reads a file…")
- A parameter schema — what arguments it accepts, machine-checkable
- An
executefunction — the part that actually does something
That's the whole interface. The model never touches your filesystem directly; it can only ask for a tool call, in JSON, and let the harness run it. This request/execute separation is where all the safety lives — between the ask and the act, Pi gets to inspect, refuse, or reshape anything.
The four (plus three)
Pi's default kit is deliberately tiny:
| Tool | What it does |
|---|---|
read |
Read a file (with offset/limit for big ones) |
write |
Create or overwrite a file |
edit |
Patch a file — exact-match old text → new text |
bash |
Run a shell command (with a timeout) |
Three more — grep, find, ls — exist as read-only extras you can switch on through tool options. And that's the entire built-in toolbox. No web browser, no database client, no deploy button.
Before you scoff: remember the bash tool exists. A model with a shell can already do almost anything a developer does — including reading READMEs to teach itself the tools it doesn't have. Pi's bet is that a sharp general-purpose hand beats a drawer of seventeen specialty gadgets. (And if you disagree, extensions let you add your own tools — more on that later.)
The five checkpoints between "ask" and "act"
When the model requests a tool call, Pi doesn't just run it. The request walks a short pipeline — think airport security, five stations:
1. Bag repacking (prepareArguments). Models have quirks: some serialize arrays as strings, some pass numbers where strings belong. This step quietly normalizes known quirks before anyone else sees the arguments.
2. Security scan (schema validation). The arguments get checked against the parameter schema at runtime. Wrong type? The call stops here and an error message goes back to the model. Tools never receive garbage.
3. The bouncer (beforeToolCall). A hook where extensions get veto power. Return { block: true, reason: "..." } and the call never runs — the model just gets the reason and has to make a new plan. This is how people build permission popups, allowlists, or "no rm -rf ever" rules on top of Pi.
4. The actual work (execute). Only now does the function run. It receives an AbortSignal (so your Ctrl+C reaches it) and an onUpdate callback for streaming progress — that's how the bash tool shows you output line by line instead of going silent for thirty seconds.
5. Last look (afterToolCall). A hook to post-process the result: sanitize secrets, write audit logs, or set terminate: true ("we're done here, stop the loop").
Any station can fail. None of them throw their way out of the building.
The golden rule: errors are messages, not crashes
This deserves its own spotlight because it's one of the best ideas in agent engineering.
When a tool fails — file not found, command exits with code 1, edit target doesn't match — Pi catches it and converts the failure into a perfectly ordinary tool result marked isError: true. The model receives it like any other result and decides what to do: retry with a corrected path, try a different approach, or tell you what went wrong.
Compare that to the naive version, where an exception tears down the whole run and you're left restarting. In Pi, failure is just input. A model that can see its own mistakes can fix them — you watch it course-correct in real time, and it feels startlingly competent.
One pro tip from Pi's own tools: specific error messages make the model dramatically better at recovering. "File not found" is okay; "File src/auth.ts not found — directory contains auth.old.ts and authz.ts" gives the model something to actually work with.
The honest part: Pi has no permission system
Most agents pepper you with "Allow this? Allow that?" popups. Pi's maintainers looked at that pattern and concluded it mostly produces popup fatigue — users clicking allow without reading, which is security theater rather than security.
So Pi's official stance, straight from its README: it runs with your user's permissions and no built-in restrictions. If you want a real boundary, put one around it:
- Plain Docker — run the whole
piprocess in a container - Gondolin — a Pi extension that keeps auth on your machine but routes tool execution into a Linux micro-VM
- OpenShell — a policy-controlled sandbox
Combined with git (your universal undo button), that's the intended safety story: isolate the process, version-control the damage, keep your eyes on the stream. Some people find this terrifying. Fans find it liberating. Either way, it's refreshingly explicit — you'll never wonder what Pi is or isn't protecting you from, because it tells you in writing.
What you can take from this
Even if you never touch Pi's code, three ideas here are worth stealing for any agent you build:
- Validate at the boundary. Models produce approximate JSON; treat every call as untrusted input.
- Turn failures into feedback. An error the model can read is a problem half-solved.
- Make power explicit. A toolset with no magic and no hidden restrictions is easier to trust than one pretending to guard you.
Sources & further reading: Security · Containerization · Extensions