Two terminal coding agents. Almost the same four-tool core. Opposite bets on who owns the harness.
The harness is everything wrapped around the language model to turn it into an agent: the tools it can call, the permission checks, the planning flow, the way sub-tasks and long-running jobs are managed. The model reasons; the harness decides what it is allowed to do and how. That wrapper, not the model, is what these two tools disagree about.
Claude Code decides the workflow for you and ships the machinery to run it: plan mode, sub-agents, background jobs, hooks, permissions. Pi, by Mario Zechner, ships almost none of that on purpose and hands you the wiring instead.
They differ on eight decisions. Each one below is hands-on, with commands you can run yourself.
The one idea that explains everything
Pi’s whole design falls out of one goal, in Mario Zechner’s own words: to “build myself a tool where I’m in control as much as possible.” Two things give him that control: context engineering, deciding exactly what enters the model’s context window, and observability, seeing at any moment what the agent and every helper it spawned are doing.
His recurring critique is that most agents surrender both the instant they get “helpful.” A sub-agent runs and you get a summary, not the steps. A background job runs and you cannot query it. Plan mode reasons in a buffer you cannot version. His fix is blunt: refuse the feature, and use a real Unix tool you can already watch.
graph LR A[Bring your own wiring<br/>Pi] -->|you assemble more| M((who owns<br/>the harness?)) M -->|it decides more| B[Batteries included<br/>Claude Code]
Keep that spectrum in mind. Every section below is the same spectrum, viewed from one feature.
Part A: the inventory of what Claude Code decides for you
The opinionated surface of Claude Code, and Pi’s answer to each. The numbered sections that follow go hands-on with the ones worth seeing in action.
| Decision | Claude Code | Pi |
|---|---|---|
| Default tools | Rich set (read, write, edit, bash, grep, glob, web, task, todo, MCP) | Four on by default: read, write, edit, bash. grep, find, ls are built in but off unless you allowlist them |
| Plan mode | Built in (Shift+Tab) | None. Write a plan to a file |
| Sub-agents | Built in (Task tool), agent may self-spawn | None. Spawn a Pi in a tmux window |
| Background jobs | Built in (run_in_background) | None. Run it in a tmux window |
| To-dos | Built in (TodoWrite) | None. Write checkboxes to a .md file |
| Approval | Built-in permission prompts | None. Use a container, or write an extension |
| Hooks | Declarative, in settings.json | Code, via a TypeScript extension |
| MCP (Model Context Protocol) | Built in | Not built in (add via extension) |
| Session shape | Linear transcript | Tree (branch in place) |
| Providers | Claude-first | 15+ providers, /model to switch |
tmux runs through Pi’s half of this table, so define it once: it is a terminal multiplexer, a tool that runs many shell sessions in windows and panes you can detach from and reattach to. To run the examples yourself, install Pi first.
Install Pi
On macOS, Homebrew is the cleanest path:
brew install pi-coding-agent$ pi --version
0.80.3
Then authenticate a provider, or pi -p has no model to call:
pi # start Pi
/login # OAuth (Claude Pro/Max, ChatGPT) or paste an API key
/model # pick the model these examples should use (I picked Sonnet 4.6)Billing note. If you log in with a Claude subscription, Pi warns that third-party harness usage draws from extra usage and is billed per token, not against your Claude plan limits. Watch it at claude.ai/settings/usage. An API key is billed per token either way.
Credentials persist in ~/.pi/agent/auth.json. Other platforms:
Pi install docs. For the other half of the
examples, install Claude Code.
1. A normal session: the toolbox you start with
Concept. Before any workflow, an agent hands the model a set of tools. That set is the agent’s opinion about what coding is.
Claude Code hands the model a loaded workbench: read, write, edit, bash, plus grep, glob, web fetch, a task spawner, a to-do tracker, and any MCP servers you wired up. You start rich.
Pi hands the model four tools: read, write, edit, bash. That is the entire
default surface. You can even choose which tools to start with. For a read-only session,
allowlist every reading tool and omit every writing one:
pi --tools read,grep,find,ls -p "Which tools are enabled?"I have access to the following tools:
1. read - Read file contents (supports text files and images)
2. grep - Search file contents for patterns (respects .gitignore)
3. find - Find files by glob pattern (respects .gitignore)
4. ls - List directory contents
No write, edit, or bash: the session literally cannot change anything.
Mental model. Claude Code is a fully-stocked workbench. Pi is four good hand tools and a clean bench you extend yourself.
Tradeoff:
- Claude Code: rich defaults mean less setup and more the model can do unprompted.
- Pi: a tiny surface means less to reason about and a smaller blast radius, but you assemble the rest.
2. Plan: a mode you approve vs a file you commit
Concept. Planning is letting the agent think through an approach before it touches code.
Claude Code. Press Shift+Tab to cycle into plan mode. The agent researches
read-only, then shows a plan and waits. You approve, and it executes. The plan lives in
the session and disappears with it.
Pi. There is no plan mode. You run a read-only session that writes the plan to disk, then execute it in a fresh session.
# Session 1: plan only, no code changes possible
pi --tools read,grep,find,ls -p "Study the auth module. Write a migration plan to PLAN.md."
# The plan is now a file you can review, edit, and version
git add PLAN.md && git commit -m "plan: auth token migration"
# Session 2: execute against the artifact, with a clean context
pi -p "Execute PLAN.md step by step. Check off each item as you finish it."Session 1 leaves an artifact on disk that you can read, edit, and commit:
# PLAN.md
- [ ] 1. Inventory every call site of issueToken() (grep)
- [ ] 2. Add AuthV2.issue() beside the legacy path
- [ ] 3. Migrate call sites one file at a time
- [ ] 4. Add an integration test for the V2 flowSession 2 reads that file and ticks each box as it finishes.
Mario’s reasoning is explicit:
“Unlike ephemeral planning modes that only exist within a session, file-based plans can be shared across sessions, and can be versioned with your code.”
And his complaint about the built-in version:
“I need observability for planning and I don’t get that with Claude Code’s plan mode.”
Mental model. Claude Code’s plan is a conversation you approve. Pi’s plan is a document you commit. One is a moment; the other is an artifact.
Tradeoff:
- Claude Code: the mode is faster and needs no discipline.
- Pi: the file survives, diffs, and travels to the next session or the next teammate.
3. Sub-agents: a summary you trust vs a pane you watch
Concept. A sub-agent is a second agent the main agent starts to do a scoped job, big enough to want its own context, scoped enough to skip a full planning session:
- mapping every call site of a function
- migrating a deprecated API across dozens of files
- reviewing a change with several reviewers in parallel, each in its own clean context and role (engineering, QA, security), with the parent aggregating their verdicts
That last one is where a sub-agent earns its keep. A single clean-context review needs
only a fresh session; running many at once and merging the results is the job a lone
session cannot do. And if you want those parallel reviewers on different vendors’
models, Claude, GPT, and Gemini side by side, that is Pi’s edge: it is multi-provider,
while Claude Code’s Task sub-agents stay on Claude models.
Claude Code. Claude Code creates a sub-agent through its built-in Task tool,
either on its own initiative or because you asked. The sub-agent runs in its own context
window and hands the orchestrator a final report, not its steps. Those steps are not
lost, though. The activity streams in the UI, and every step is written to a per-agent
log at ~/.claude/projects/<project>/<session>/subagents/agent-*.jsonl. So the
sub-agent is observable after the fact and on disk. What it is not: present in the
parent agent’s context, or sitting inline in your main scrollback.
Mario’s objection is aimed at that first gap, the agent-to-agent handoff:
“You have zero visibility into what that sub-agent does. It’s a black box within a black box.”
He is not against sub-agents wholesale, though. His own most common use is a single code-review sub-agent, spawned via bash; the anti-pattern, in his words, is spawning many in parallel to implement features.
Pi. No sub-agent tool. The official stance is to bring your own, quoted verbatim from Pi’s README:
“No sub-agents. There’s many ways to do this. Spawn pi instances via tmux, or build your own with extensions, or install a package that does it your way.”
The community took that literally.
pi-side-agents, by Petr Baudis, is a real
installed package that adds an /agent command. Each child runs in its own tmux window
and git worktree, and the parent watches or steers it.
The package has two prerequisites, because of how it isolates each child:
- tmux, with Pi started inside a tmux session (each child gets its own tmux
window). Miss it and
/agentfails withtmux is required for /agent but was not found. - A git repository, because each child runs in its own git worktree, isolating its
branch so the parent and children never collide. Run it in a plain folder and
/agentfails withfatal: not a git repository. Rungit initfirst if needed.
brew install tmux # if you don't have it
# Optional, and not required for /agent: tmux strips key-modifier info, so Shift+Enter
# and Ctrl+Enter collapse to plain Enter in Pi. Turn on extended keys so those keys survive:
echo 'set -g extended-keys on' >> ~/.tmux.conf
echo 'set -g extended-keys-format csi-u' >> ~/.tmux.conf
tmux # start a session, THEN launch Pi inside it
pi install npm:pi-side-agents # one-time installThen inside Pi:
/agent Map every call site of parseToken() and write findings to NOTES.md
/agents # live overview of every running child
Each child is a real pi running in its own tmux window. A Claude Code sub-agent hands
the parent a report; a Pi child is a live process you can switch to, watch, or query.
Reading and steering a child. Do it through the parent, from your main Pi window.
Start with /agents:
/agents
[side-agents-report]
side-agents
i-ll-review-2 waiting_user win:#1 worktree:myapp-agent-worktree-0001
task: review .../2026-07-01-claude-code-vs-pi.md's section 3. Sub-agents
That is a status row, not the review. Read it left to right:
i-ll-review-2is the child’s id, how you address it.waiting_useris its status: it finished and is blocked on a question to you. Other states you will see arerunning,failed, andcrashed.win:#1is its tmux window.worktree:...is its git worktree: a second working directory checked out to the child’s own branch (a sibling folder like../myapp-agent-worktree-0001), so it edits files without touching your main checkout.task:is the prompt it was given.
waiting_user is a pause, not an ending. A row only lingers in /agents while it is
in-flight or blocked; a clean exit deletes itself, so a visible row is always something
still running, waiting on you, or broken:
stateDiagram-v2 running --> waiting_user: asks you something waiting_user --> running: you reply running --> done: exit 0, auto-removed running --> failed running --> crashed
To read its actual output, ask the parent in plain English, and Pi pulls the child’s text into your session (clean, no raw pane capture):
Show me side-agent i-ll-review-2's output.
Because it is waiting_user, you answer it the same way, and the parent relays your
reply to the child:
Reply to i-ll-review-2: no edits, just summarize your top three findings.
Prefer to watch it live? Switch to its window with Ctrl-b 1 (children are windows 1,
2, …; your Pi is window 0). If the pane ignores your keys, press q first: you are in
tmux scroll mode, not stuck. Ctrl-b 0 returns to Pi. Do not capture-pane a live
agent window.
Finishing a child. A waiting_user agent waits indefinitely, holding its window,
worktree, and branch, until you act. Two ways out:
- Finish it cleanly. Reply so it resumes and exits. It runs to
done(exit 0), auto-removes from/agents, and closes its own window, leaving nothing to clean up:Reply to i-ll-review-2: no further action, you're done. - Abandon it. Kill the window and remove the git leftovers yourself:
tmux kill-window -t @1 git -C <repo> worktree remove --force myapp-agent-worktree-0001 git -C <repo> branch -D side-agent/i-ll-review-2 git -C <repo> worktree prune
Mental model.
graph TD subgraph Claude Code P1[Main agent] -->|Task| BB[sealed box] BB -->|final report only| P1 end subgraph Pi P2[Main agent] -->|tmux window| GP[glass pane] GP -->|every step visible| You[you: switch to its window] GP -->|agent-check, agent-send| P2 end
Tradeoff:
- Claude Code: the sealed box keeps your context clean and quiet; the
Tasktool needs no setup and leaves no trace. - Pi: the glass pane keeps you in control and shows you when the helper goes wrong, but that visibility is a raw terminal you have to wire and mind.
The catch: this is wiring, and wiring has edges. Getting one sub-agent running took:
- a package install (
pi-side-agents) - installing tmux
- an extended-keys tweak in
~/.tmux.conf - launching Pi inside tmux
git initplus a first commit- enough tmux fluency to move between windows
The observability is real, but it is a raw terminal: point the wrong command at a live
agent pane and you can wedge your terminal. Claude Code’s Task tool does none of this
to you, and shows you none of it.
4. Background jobs: a tool you poll vs a pane that is just there
Concept. A background job is a long-running process, like a dev server or a test watcher, that must keep running while the agent does other things.
Claude Code. You ask in plain English:
Start a static file server on port 8199 in the background.
It runs a Bash command with run_in_background: true, which returns a shell id:
Command running in background with ID: bpug4gf33
The agent keeps working, then reads new output on demand with the BashOutput tool,
targeting that id:
::1 - - [02/Jul/2026 07:46:41] "GET / HTTP/1.1" 200 -
::1 - - [02/Jul/2026 07:46:41] code 404, message File not found
::1 - - [02/Jul/2026 07:46:41] "GET /missing HTTP/1.1" 404 -
It stops the job with KillShell. Note the shape of this: the output is not on screen,
it is buffered behind a tool call you have to make. You poll to see it.
Mario’s critique names the hidden cost:
“Background process management adds complexity: you need process tracking, output buffering, cleanup on exit, and ways to send input to running processes.”
He also recalls a concrete failure:
“in earlier Claude Code versions, the agent forgot about all its background processes after context compaction and had no way to query them.”
That bug is fixed, but the point stands: background state lives inside the agent, so it can be lost.
Pi. There is no background-bash tool, because plain bash plus tmux already covers
it, and you ask in the same plain English:
Start the dev server in a tmux window called dev so it keeps running.
Pi puts it in a detached tmux window (that returns instantly, so the agent is not blocked), then reads that window whenever you or it wants:
tmux new-window -d -n dev 'npm run dev' # runs in its own window, returns at once
tmux capture-pane -p -t dev -S -30 # read the last 30 lines any timeVITE ready in 412 ms
Local: http://localhost:5173/
The job is a real tmux window, not agent state: you can open it yourself with
Ctrl-b 1, and it survives context compaction because Pi never had to remember it.
Tradeoff:
- Claude Code: zero setup, but the job is agent state. Its output stays hidden until
the agent polls
BashOutput, and the agent has to remember the job is even running. - Pi: you own a tmux window, but the job is just there. Glance at it any time, nothing to poll, and nothing for the agent to forget.
5. Observability: prove it to yourself
This is the spine of the whole comparison, so do it by hand.
With Claude Code. Ask it to spawn a sub-agent and start a dev server, then ask what each is doing right now. The information exists, but it lives in three different places:
- the sub-agent’s live activity, streamed (and collapsible) in the chat UI
- that sub-agent’s full transcript, written to a per-agent log file at
~/.claude/projects/<project>/<session>/subagents/agent-*.jsonl - the server’s output, which you see only by asking the agent in chat to check it
(“what is the server printing now?”); it runs the
BashOutputtool against the background shell id and returns a one-time snapshot into the conversation
None of the three is your main scrollback, and the orchestrating agent works from the sub-agent’s summary, not its steps. Observability is available, not immediate.
With Pi. Everything a helper does is a process in a pane. One command lists every helper you have running:
tmux lsdev: 1 windows (created Wed Jul 1 09:14:02 2026)
research: 1 windows (created Wed Jul 1 09:15:20 2026)
Attach to any of them and you are looking at the real thing:
tmux attach -t researchIf you use pi-side-agents, its /agents command gives you the same roll-up from
inside Pi, and its parent tools (agent-check, agent-wait-any, agent-send) let the
orchestrator poll and steer each child without ever losing sight of it.
Mario sums up the contrast in four words after showing Pi debugging a crashing C program in a pane: “How’s that for observability?”
Be fair to Claude Code. The context-isolated sub-agent and the polled job are not bugs. They keep the parent context small and your screen quiet, which matters when the model has to reason over everything in view. Its steps are still on disk if you need them. Pi trades that quiet for glass walls, everything in a pane at a glance. Pick the trade on purpose.
6. Approval: prompts built in vs a gate you build
Concept. Approval is the gate that stops an agent before a risky action, like
rm -rf or a force push.
Claude Code. A permission system ships in the box. It prompts before risky actions,
remembers an allowlist, and offers modes from cautious to bypassPermissions.
Pi. No permission popups by default. You get two honest choices.
Contain it. Run the whole pi process inside an isolated environment you set up, so
nothing it does can reach the host. Pi does not ship a sandbox; you build the isolation.
Pi’s containerization docs give three
patterns, and a DevContainer is a natural fourth:
- plain Docker: build a
Dockerfile.piand run Pi in it (docker build -t pi-sandbox -f Dockerfile.pi .), per the containerization docs - the
gondolinmicro-VM extension: routes tools into a local micro-VM, auth stays on the host - the separate
openshelltool: a policy-controlled sandbox - a DevContainer: your editor’s
.devcontainerrunning Pi inside Docker (not in Pi’s docs, but the same boundary)
Gate it. Add approval in code, with an extension that intercepts tool calls before they run:
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" &&
event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("Dangerous!", "Allow?");
if (!ok) return { block: true, reason: "Blocked by user" };
}
});Mental model. Claude Code ships the safety checks and runs them for you, prompting before risky actions on day one. Pi ships none and hands you the raw materials: wall the whole process off in a container you built, or write the checks yourself in an extension. Either way, the safety is yours to design and yours to get right.
Tradeoff:
- Claude Code: built-in prompts are safe on day one with zero setup.
- Pi: the DIY gate is silent until you build it, but then it is yours and it does precisely what you decided.
7. Hooks: config you declare vs code you write
Concept. A hook is code that runs automatically at a defined point in the agent’s lifecycle, so you can enforce a rule without asking every time.
Claude Code. Hooks are declarative, in settings.json. You match an event and run a
shell command. This one guards every Bash call:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "python3 guard.py" }]
}
]
}
}Pi. Yes, Pi has hooks, but as code inside a TypeScript extension, not as config. The event API covers the same ground:
pi.on("before_agent_start", async (event, ctx) => { /* edit the prompt */ });
pi.on("tool_call", async (event, ctx) => { /* block or allow */ });
pi.on("tool_result", async (event, ctx) => { /* rewrite output */ });
pi.on("session_start", async (event, ctx) => { /* set up resources */ });
pi.on("session_shutdown", async (event, ctx) => { /* clean up */ });Mental model. A Claude Code hook is a tripwire you declare and point at a script. A Pi hook is a function you write inside the agent’s own runtime, with the full API in reach.
Tradeoff:
- Claude Code: declarative config is quick and language-agnostic.
- Pi: in-process code is more power and more responsibility, because the extension runs with full system permissions.
8. Session history: a list you scroll vs a tree you branch
Concept. The session is the saved record of a run. Its shape decides whether you can go back and try a different path.
Claude Code. A linear transcript. It is stored as JSONL under
~/.claude/projects/<encoded-path>/<uuid>.jsonl, and you resume it with --continue or
--resume. History is a list: one line after another.
Pi. A tree. The file is still JSONL, at ~/.pi/agent/sessions/, but every entry
carries an id and a parentId:
{"type":"message","id":"a1b2c3d4","parentId":"prev1234","timestamp":"2024-12-03T14:00:01.000Z","message":{"role":"user","content":"Hello"}}That parentId links each entry back to the one before it, so the file forms a tree
instead of a flat list. /tree lets you jump to any past entry and continue from there,
which starts a new branch inside the same file:
[user] ── [assistant] ── [user] ── [assistant] ─┬─ [user] <- current leaf
│
└─ [branch_summary] ── [user] <- alternate branch
graph TD subgraph Claude Code: a list L1[msg] --> L2[msg] --> L3[msg] --> L4[msg] end subgraph Pi: a tree T1[msg] --> T2[msg] --> T3[msg] T3 --> T4a[branch A] T3 --> T4b[branch B] end
Tradeoff:
- Claude Code: a list is simple and plays perfectly with terminal scroll and search.
- Pi: a tree lets you explore a second approach without throwing away the first, at the cost of a more complex mental model.
When to reach for which
- Reach for Claude Code when you want the batteries: fast onboarding, safe defaults, and a workflow that is decided for you so you can just build.
- Reach for Pi when you want the wires: total visibility, a tiny surface you fully understand, and a harness you shape to your own workflow.
Neither is “better.” They optimize different things. Claude Code optimizes the median session out of the box. Pi optimizes your control over every part of it.
Key takeaways
- The split is one value: Claude Code trades visibility for convenience; Pi trades convenience for visibility.
- Pi’s four-tool core (
read,write,edit,bash) is the smallest honest definition of a coding agent. Everything else is a choice you make. - Every “missing” Pi feature maps to a Unix primitive you can already watch: sub-agents and background jobs become tmux windows; plans and to-dos become files.
- Claude Code’s sub-agents and background jobs are not flaws. They keep context small. That is a real benefit, and a real blind spot.
- Hooks exist in both, but as config in Claude Code and as in-process code in Pi. That mirrors the whole design: declared for you, or written by you.
- Session shape is the quiet tell: a list optimizes for one path, a tree optimizes for exploring many.
Try it yourself
- Install Pi:
brew install pi-coding-agent(or the installer). - Run a read-only planning session and commit the
PLAN.mdit writes. - Open a second tmux window, spawn a Pi to execute the plan, and watch it live by switching to its window.
- Then run the same task in Claude Code with plan mode and a
Tasksub-agent, and notice what you can and cannot see. - Decide, for your work, whether you want the batteries or the wires.
References
- Pi docs: pi.dev/docs/latest (tools, extensions, session-format).
- Mario Zechner, “What I learned building an opinionated and minimal coding agent” (source of the Mario quotes in this post).
- Contributor packages that ship the tmux recipes Pi itself does not:
pi-side-agents(Petr Baudis),pi-tmux(offline-ant),pi-interactive-subagents(HazAT). - Claude Code docs: hooks, sub-agents, and background bash.