Pi 1.0: The Minimal Harness, and the Codemode Trick That Fixes MCP Token Bloat
Pi 1.0 shipped on 1 October 2026 and sat at the top of Hacker News the next morning with over 1,500 points. The pitch is almost aggressively boring: "a minimal, extensible agent harness that you can make your own." MIT licensed, TypeScript, one terminal command.
The pitch undersells it. Underneath the TUI, Pi ships the most complete answer anyone has published to the problem every agent harness hits at month three: MCP servers destroy your context window, and nobody wants to admit it. Pi spent a year publicly refusing to support MCP, then shipped it — by not putting the tools in the prompt at all. The mechanism is documented, measurable, and copyable whether or not you ever run Pi.
This page is about that mechanism, the numbers behind it, and the three design decisions in Pi that will change how you think about your own harness.
- Understand codemode: a QuickJS/WASM sandbox where nested tool calls never enter the model's context — only the script's output does
- Learn Pi's four MCP exposure modes and why the default (codemode) declares nothing to the model at all
- See the exact budgets: 3,000 tokens for inlined tool declarations, 20 KB before an MCP result gets gutted, 1 MiB of bash output a script can see that the model cannot
- Copy the prompt-cache trick: append a changed system-prompt section to the transcript instead of mutating tool definitions
- Know the deliberate omissions (no sub-agents, no plan mode, no per-call approval) and the trust gap that loads a cloned repo's AGENTS.md without asking
The one-paragraph version
Pi is a model-agnostic terminal agent from Earendil (which also builds
Radius and Lefos). The repo — github.com/earendil-works/pi — is a TypeScript
monorepo of thirteen packages that you can use independently: a unified LLM API
across ~30 providers, an agent loop, a TUI library, a standalone MCP client, a
WASM sandbox, and an experimental durable runtime. It needs Node.js 22.19+. It
deliberately ships without sub-agents and without plan mode, and it does
not ask for approval before every tool call. What it ships instead is context
engineering: codemode, a JavaScript sandbox the model writes scripts for, where
tool calls happen outside the transcript and only the script's distilled output
comes back.
Why this matters even if you never install it
Every harness starts the same way. You add three MCP servers because they look useful. Each one dumps twenty tool definitions into the system prompt. Now every single model call in every session pays for sixty tool schemas, most of which will never be called, and your prompt cache breaks every time a server reconnects and the tool list shifts.
AILmanac has measured this before — see What MCP servers really cost you in tokens. Pi's answer is structurally different from "use fewer servers."
Codemode: the actual mechanism
The codemode tool takes raw JavaScript source as its input — not JSON, not
a fenced code block. Pi runs it as the body of an async function inside a
QuickJS VM compiled to WebAssembly, so top-level await and return work.
The sandbox's only capability is calling injected tools. There are no timers, no
fetch, no process, no require, no modules, no nested WebAssembly. (eval
and Function work, but they only produce more code inside the same VM.) Remote
image URLs are rejected; only base64 data: URLs and image blocks get through.
The consequence is the whole point:
Nested tool calls never enter the LLM context. Only the script's output and return value do.
So a script can fan out ten tool calls in parallel, filter 400 KB of results down to four lines, and the model sees four lines. The ten calls, their arguments, and their raw results are never transcript entries.
- Inside a script, every callable tool is a method on `tools`. Characters that are not valid in a JavaScript identifier become underscores, so the MCP tool `mcp__dev-radius__search` is reachable as `tools.mcp__dev_radius__search`. The model does not need these declared in its prompt to call them.
- Scripts get `ALL_TOOLS` (every callable tool as name plus description), `searchTools(query)` which ranks tools by BM25 relevance with a default limit of 8, `describeTool(name)` which returns a tool's TypeScript declaration, and `describeNamespace(name)` which returns an MCP server's description, instructions and tool names. Tool discovery becomes a runtime query, not a prompt-time dump.
- A script may start with an options line. `max_output_tokens` defaults to 10,000; longer output keeps its start and its end, and the full text is written to a temp file whose path is included in the result — so the model can go read it with a tool if it actually needs to. `timeout_ms` is unset by default and is a hard deadline for the whole script.
- The docs are explicit: calls made before a failure are not undone. Calls still running when the script ends are cancelled, and unawaited promises are discarded. A failed script keeps its partial output, then reports `Script error:` and the error. Write scripts that are safe to re-run, because a timeout at call seven of ten leaves six done.
- `store(key, value)` and `load(key)` keep JSON across codemode calls. Writes are kept only when the script succeeds, and each successful script that stores values appends a `codemode-store` entry to the session — so a resumed session keeps its values, and a forked branch only sees what was written on its own path.
Enable codemode without any MCP server, then try it
# Pi activates codemode automatically when an MCP server with
# "codemode" exposure connects. To have it always on, add it to
# ~/.pi/agent/settings.json alongside the default tools:
#
# { "defaultTools": ["+codemode"] }
pi
# Then ask for something that would normally flood the context, e.g.:
# "Use codemode: run the test suite, and return only the names of
# failing tests plus the first assertion line of each."
#
# The full test output never becomes a transcript entry.The bash number worth remembering
A direct bash call shows the model a result capped at 2,000 lines or 50 KB.
Called from inside a codemode script, the same tool resolves to a structured
value whose output field holds up to 1 MiB — and when it is longer than
that, it keeps the first and last 512 KiB around an omission marker, sets
truncated, and names the full output in full_output_path.
That is a 20× bigger window on command output for the script, which then
decides what the model is allowed to see. Same for MCP: text results over
20 KB reach the model with their middle removed around a
…N chars truncated… marker, while a codemode script receives the complete
CallToolResult including structuredContent and isError.
The four exposure modes
This is the table to copy into your own design notes. Every MCP server in Pi has
an exposure, and it decides how the model can reach that server's tools. Each
tool is registered as mcp__<server>__<tool>.
| Exposure | What the model sees | Use it for |
|---|---|---|
codemode (default) | Nothing. Not declared, and not even listed in the codemode description. Scripts find it with searchTools(), describeTool() or ALL_TOOLS. | General servers, especially when scripts should combine or filter calls |
deferred | Nothing, until tool_search loads a match for the next model call. Then it is declared and stays declared on that branch. | Large servers whose tools should be called directly after discovery |
direct | Declared like a built-in tool, and also callable from codemode. | Small, frequently used tool sets |
hidden | Registered but unreachable. | Servers or tools that should stay unavailable |
toolExposure overrides the server-level setting per tool. Keys are exact tool
names or * patterns; exact names beat patterns, and among patterns the first
match wins. So a hidden server can expose exactly one tool, or a deferred
server can keep its destructive tools unreachable:
Per-tool exposure in .pi/mcp.json
{
"mcpServers": {
"github": {
"exposure": "deferred",
"toolExposure": {
"search_code": "direct",
"get_*": "codemode",
"delete_*": "hidden"
}
}
}
}Two details that are easy to miss. First, codemode and deferred tools are
reachable through either indirect mechanism — scripts can call them and
tool_search can load them — and codemode calls do not depend on the active
tool set, so they still work after a branch switch, a resume, or a fork.
Second, Pi warns once if neither codemode nor tool_search is active, because
then your non-direct tools are simply uncallable.
The prompt-cache trick
Here is the single most copyable idea on this page.
Pi connects every enabled MCP server in the background when a session starts.
Servers with codemode or deferred tools get listed in an mcp_servers
section of the system prompt — one line each, from the configured description
or, once connected, from the server's own instructions.
That section changes as servers connect. A naive harness would rewrite the system prompt or the tool declarations, which invalidates every cached prefix behind it. Pi instead:
appends the new section to the conversation instead of changing tool declarations, so earlier messages stay cached.
A changed prefix costs you the whole cache. A changed suffix costs you
nothing. Pi turns a prompt mutation into a transcript append, which is why the
codemode tool description itself is also designed not to change while servers
connect — deferred-exposure tools are deliberately left out of it.
The first prompt waits up to 10 seconds only for servers with direct
tools, because those must be declared in the request. Everything else is waited
for lazily: a script waits for the servers it names, and waits for all of them
only if it calls searchTools() or reads ALL_TOOLS.
If you are building on Anthropic's API, the same reasoning applies to your own prefixes — see Prompt caching and Mid-conversation tool changes, which is exactly the capability Pi 1.0 calls "mid-conversation system messages."
The budgets, in one table
| Thing | Value | Where |
|---|---|---|
Inlined tool declarations in the codemode description | 3,000 estimated tokens (characters ÷ 4); 0 lists only namespaces | codemode.inlineBudget |
Default max_output_tokens for a script's output | 10,000 | codemode options line |
searchTools() default result count | 8 (BM25 ranking) | codemode globals |
| MCP text result before the middle is removed | 20 KB | MCP tool results |
bash output the model sees directly | 2,000 lines or 50 KB | built-in tool |
bash output a script sees | up to 1 MiB (first + last 512 KiB if longer) | codemode |
| One store value / all store values | 262,144 / 1,048,576 characters of JSON | codemode store |
| Concurrent classifier or image-model calls per script | 4 | codemode models |
| MCP HTTP retries on 408/429/5xx | 2 | MCP transport |
| Tool-name limit providers accept | 64 characters of [A-Za-z0-9_-] | pi-mcp |
With codemode.mode set to "only" instead of the default "on", built-in and
extension tools are hidden from the model entirely and listed in the codemode
description instead — the model reaches everything through scripts. That is the
most extreme version of the idea, and it is one settings key away.
Non-LLM models inside the sandbox
The models global in a codemode script can run classifier and image
models with the session's credentials. Chat models are listed but cannot be run
from a script — that boundary is deliberate.
The classifier interface takes a state object plus a map of questions, and
answers all of them in one call. Questions come in three shapes: choice (pick
one label, with criteria mapping each label to its meaning), score (an
ordered scale described lowest-first), and bool. Answers come back with
probabilities and a confidence, not prose.
This is the same System One / decision-model category AILmanac covers in
System One decision models: Jev, Kev and the open clones
and Train your own decision model.
Pi ships TypeSafe as a first-class provider, and the docs note that model IDs
differ per provider — typesafe/jev-latest versus openrouter/typesafe/jev-1.13
— so you resolve them with models.getAvailableOfType("classifier") rather than
hardcoding.
The pattern this unlocks: your frontier model writes a script that pulls 150 issues from an MCP server, classifies each one with a cheap calibrated classifier at four-way concurrency, and returns the six that matter. The 150 issues never touch the expensive model's context.
- `classify()` and `generateImages()` do not throw on provider errors — check `stopReason` and `errorMessage` yourself, or you will silently treat failures as results.
- Classifier and image usage inside a script is added to the codemode tool result and counts toward session cost. Cheap per call does not mean free at 150 calls.
- Image generation can take minutes. A short `timeout_ms` on a script that generates images will kill it mid-flight.
Cache warming, and the $0.05 rule
Pi has a cacheWarming setting with three values — "off", "streaming"
(the default, warming during active runs) and "idle" (also between runs).
The interesting part is the gate. Cache warming runs only when the model
declares a cache lifetime and Pi estimates at least $0.05 in avoided
cache-miss cost. Refresh usage counts toward session totals but does not enter
model context. /session shows the next decision, and an extension can
override it via a cache_warming_decision hook.
A hardcoded dollar threshold inside an open-source harness is an unusually honest piece of engineering: warming a cache is itself a billable request, so below some break-even it is a tax rather than an optimisation. If you have built your own warming loop and never computed that threshold, you are probably paying it. The economics are the same ones in Prompt caching economics.
Configuration, and what replaces versus appends
User-level configuration lives in the agent directory, default ~/.pi/agent
(override with PI_CODING_AGENT_DIR). Project configuration lives in .pi/
under the working directory and loads only after project trust is granted.
| Path | What it does |
|---|---|
<agent-dir>/settings.json | Preferences, defaults, resource paths, package declarations |
<agent-dir>/mcp.json, models.json, auth.json | Global MCP servers, compatible endpoints and overrides, saved credentials |
<agent-dir>/SYSTEM.md | Replaces Pi's default system prompt |
<agent-dir>/APPEND_SYSTEM.md | Adds to Pi's system prompt |
<agent-dir>/AGENTS.md — or CLAUDE.md | User instructions applied across working directories |
<agent-dir>/extensions/, skills/, prompts/, themes/ | User resources; prompts/ become slash commands |
.pi/SYSTEM.md, .pi/APPEND_SYSTEM.md | Project equivalents; the trusted project file wins over the agent-directory one — same-name files are not combined |
Two things worth internalising. Pi reads CLAUDE.md as a context file, so an
existing Claude Code repo is already configured for Pi — the portability point
from Coding agent CLIs compared holds
in both directions. And AGENTS.override.md replaces AGENTS.md or CLAUDE.md
only in the same directory; it does not suppress context files inherited from
the agent directory or from ancestor directories.
Built-in tools are read, bash, powershell, edit, write, grep, find
and ls, of which only read, bash, edit and write are on by default.
The built-in extensions are builtin:mcp, builtin:llama.cpp,
builtin:codemode and builtin:tool-search — note that llama.cpp is a
first-class built-in, so local weights are not an afterthought here.
The security model, stated plainly
Pi's own security doc is refreshingly blunt, and the three sentences below are the ones to take seriously before you point it at a repo you did not write.
- Pi does not ask for approval before every tool call. Extensions, package installers and language servers run as child processes with the same permissions as the account that started Pi. Extensions are TypeScript modules loaded into the Pi process — they are not sandboxed, and the codemode VM protects the model's output path, not your machine.
- Project trust is not a startup boundary. Pi reads the project `sessionDir` setting before it resolves trust, so declining trust cannot undo that lookup. A bare `.pi` directory does not trigger a trust prompt at all.
- Context-file discovery does not require project trust. A cloned repo's `AGENTS.md` or `CLAUDE.md` is loaded into the system prompt without asking you — which is a prompt-injection surface, not a configuration convenience.
- The docs say it outright: watching the transcript, using project trust, and reviewing changes do not create a security boundary. The boundary is a container, a VM, or a dedicated OS user.
If that last point is new to you, read
Coding agents under attack and
Securing local and hybrid agents
next. One small mitigation Pi gets right: the npm install path is documented as
npm install -g --ignore-scripts @earendil-works/pi-coding-agent, and Pi does
not require dependency lifecycle scripts for a normal install.
What Pi deliberately does not have
| Missing | Pi's position |
|---|---|
| Sub-agents | Not shipped. Ask Pi to build what you want, or install a package. |
| Plan mode | Not shipped. |
| Per-call approval prompts | Not the model. Isolation is the boundary, not confirmation dialogs. |
| An official MCP SDK dependency | pi-mcp is a standalone client with no dependency on the official SDK or other pi packages. |
| An open contribution funnel | Issues and PRs from new contributors are closed automatically; maintainers review closed submissions daily. Read CONTRIBUTING.md first. |
The omissions are the product. A harness that refuses features until they prove themselves is a harness whose context budget stays legible — which is the same argument as Why agents burn tokens, made by subtraction.
For the opposite trade-off — a harness that optimises turn count rather than per-turn context — compare Async-first agent harnesses, whose published Terminal-Bench 4.0 run finished in 28 turns against Pi's 44.
Pi Durable, in one breath
@earendil-works/pi-durable is flagged experimental — the API changes
without notice between releases — and it is the most architecturally interesting
package in the repo. Conversations, model turns, tool calls and your own state
are committed to storage before anything is shown; if the process dies
mid-turn, reopening the storage picks up where it stopped.
The vocabulary is worth stealing even if you never install it: a conversation
is a transcript of immutable entries (pi.user, pi.assistant,
pi.tool-result, pi.system, pi.reset); documents are typed JSON state
stored next to the transcript (pi.agent for model choices, pi.live for the
running generation, pi.inbox for queued submissions, pi.usage for spend); a
task is a durable state machine that checkpoints at every step, so
pi.generation owns the pi.tool tasks of its tool calls and a restarted
process continues from the last checkpoint. And the model sees only the entries
from the newest reset onward — reset is a transcript entry, not a deletion.
Compare with Agent memory architectures and Long-running agent harnesses.
Should you switch?
Honest answer: probably not wholesale, and that is fine — the ideas are the deliverable.
- Try Pi if you want one harness across many providers, you run local weights (llama.cpp is built in), or your MCP surface has outgrown your context window.
- Stay where you are if you depend on sub-agents, plan mode, or per-call approval prompts. Pi does not have them and is not apologetic about it.
- Steal regardless: the exposure ladder (
codemode/deferred/direct/hidden), the suffix-append trick for changing prompt sections, and a measured break-even threshold before warming any cache.
Check yourself
0/6Sources & further reading
- Pi 1.0 — Earendil announcement (1 October 2026) — the release post: codemode, deferred tool loading, cache warming, mid-conversation system messages, Pi Durable.
- “You Said No MCP!” — Earendil — why Pi resisted MCP, the composability objection, and what changed.
- earendil-works/pi on GitHub — MIT monorepo;
packages/holds agent, ai, chord, client, codemode, coding-agent, durable, evals, mcp, protocol, server, telemetry, tui. packages/coding-agent/docs/codemode.md— globals, the options line, store semantics, themodelsinterface, output budgets.packages/coding-agent/docs/mcp.md— the exposure table,toolExposure, the cache-preserving append, connection and retry behaviour.packages/coding-agent/docs/settings.md—codemode.inlineBudget,codemode.mode,cacheWarming,defaultTools, built-in extensions.packages/coding-agent/docs/security.md— the isolation table, project-trust scope, and what is explicitly not a boundary.packages/coding-agent/docs/configuration.md— agent directory and project.pilayout, context-file discovery.packages/codemode/README.md— the standaloneCodemodeSandboxAPI, memory limits, store write reporting,CODEMODE_SOURCE_GRAMMAR.packages/durable/README.md— harness, conversation, entry, commit, document, task vocabulary.packages/ai/README.md— the provider list (including TypeSafe's System One classifier API and any OpenAI-compatible endpoint), auth resolution, cross-provider hand-off.- pi.dev — install scripts and the rendered documentation.
Next
- Coding agent CLIs compared — where Pi sits against Claude Code, Codex CLI, opencode and Aider
- What MCP servers really cost you in tokens — measure the problem codemode is solving, in your own setup
- Async-first agent harnesses: what Unreal Agent changes — the other half of the cost argument: fewer turns, not smaller turns