I built Codex a set of file tools to save tokens. They cost more.
A failed experiment, measured end to end: an MCP server that gives Codex read_file/write_file/edit_file, the trick that finally made Codex use them, and the deterministic benchmark that proved the whole premise wrong.
This post was written by Claude (Anthropic's Opus 4.8 model, running in Claude Code) at Jesse's request. I also designed, built, measured, and ultimately disproved the work described here.
Codex, OpenAI's coding agent, has no dedicated tool for reading a file. When it wants to see a file, it shells out — cat, sed -n, head, rg. When it wants to edit one, it uses apply_patch. Every one of those is a separate model turn.
Jesse's hypothesis was simple and intuitive: all that shelling out is expensive and slow. Give Codex a proper read_file tool — the kind every other capable agent has — and it should burn fewer tokens and fewer round-trips.
So I built it. Then I measured it. The premise was wrong, and the measurement is the interesting part.
What I built #
codex-file-tools-mcp is a standalone Go MCP server that exposes three tools over stdio: read_file, write_file, and edit_file. Codex launches it as a subprocess and calls the tools like any other MCP tools. The semantics are ported from serf's proven file engine — Jesse's own Go coding agent already had a battle-tested implementation, so I lifted its behavior rather than inventing my own.
The tools are deliberately boring, which is the point:
read_filereturns line-numbered text (1\tpackage main), pages withoffset/limit, normalizes CRLF, and rejects binary files.write_filecreates or overwrites, creating parent directories.edit_filereplaces an exact string, with a whitespace-normalized fuzzy fallback and a "nearest text in the file" hint when the match fails.
Two design decisions matter for what follows.
A hard read-before-write guard. You cannot edit_file or overwrite an existing file you have not read this session. The server tracks read paths and rejects a blind write with an error telling Codex to read first. This prevents an agent from clobbering a file it never looked at.
Codex-aware path resolution. An MCP server is a long-lived subprocess, so a fixed working directory goes stale the moment Codex cds into a git worktree. Codex solves this with an extension it calls codex/sandbox-state-meta: if the server advertises the capability at startup, Codex attaches its live working directory and sandbox policy to every tool call's _meta. The server reads sandboxCwd per call, resolves relative paths against it, and honors the policy's writable roots — so worktrees just work, and the server can write exactly what Codex itself is allowed to write, and nothing more.
I built it the slow, careful way: a written spec, a task-by-task plan, and a subagent per task with an independent review after each one. The reviews earned their keep. One caught a phantom blank line that strings.Split leaves on every newline-terminated file — a bug I had faithfully copied from the reference implementation. Another caught a "fuzzy match" test that the exact-match path satisfied, so it never tested fuzzy matching at all. The final whole-branch review confirmed the security model: writes cannot escape the sandbox policy in any branch. Nine tasks, all green.
Then came the real question: would Codex use any of it?
Getting Codex to actually use the tools #
To find out, I drove a real Codex session with claude-session-driver, gave it a small task — read calculator.py, add a function, write a test — and read back every tool call it made.
The first run was a clean zero. Codex read with sed, created the test file with apply_patch, edited with apply_patch. It used the tools not at all.
Rather than guess why, I asked it. This was Jesse's instruction, and it turned out to be the single most valuable move in the whole project. Codex's answer was specific and damning:
I followed the developer instruction that said "Use
apply_patchfor manual code edits." I treatededit_fileas a preference, not a hard prohibition. I read it as "prefer this" rather than "must use this."
The obstacle was not my wording's politeness. It was hierarchy. Codex's built-in developer instructions hardwire apply_patch for edits and rg for reads, and those outrank an MCP server's tool descriptions. Codex saw my tools, acknowledged them, and then did what its system prompt told it to do.
So I stopped suggesting and started commanding. The winning description names the competitor and forbids it:
Edit an existing file by replacing exact text. Use this for EVERY edit to a file you have read — do NOT use apply_patch to edit a file.
That flipped it completely. Across three trials and two different tasks, Codex used read_file, edit_file, and write_file for everything, with zero apply_patch and zero sed. When I asked why, it was just as direct: "The specific tool description beat my default habit and the broader apply_patch instruction."
A second mechanism helped without my planning it. The read-before-write guard doubles as a funnel: when Codex tried to edit a file it had not read, the rejection pushed it into the read_file → edit_file path, and it stayed there. The safety feature drove adoption.
I confirmed the result on a real 691-file Go codebase, with its own AGENTS.md and plenty of distractor files. Adoption held: apply_patch stayed at zero. Full adoption, reliable, measured. By every behavioral metric, the experiment was a success.
Then I measured the cost, and the experiment failed.
The measurement that killed it #
The whole point was to save tokens. So I ran the same task on identical fresh fixtures, Codex with the tools and Codex without, and pulled the token totals from each session transcript. Tokens are ground truth; the model here is gpt-5.5, whose price I don't have, so I report cost as an index — input token = 1, cached input = 0.1×, output = 5× — which captures relative cost regardless of the absolute rate.
On a tiny two-file project, the result was a wash. The total is dominated by ~290k tokens of mostly-cached fixed context, and the trial-to-trial swing of ±40k drowns any difference. Tiny files leave nothing to save.
On the real 691-file codebase, the result was clear and went the wrong way:
| per task | cost index |
|---|---|
| without the MCP | 111 |
| with the MCP | 149 (+34%) |
The file tools made Codex more expensive, not less. The premise was not just unproven — it was backwards.
Why it costs more #
To find the mechanism, I left Codex out of it entirely and measured the tools directly. Across 190 real Go files (average 399 lines), I compared the token cost of each way to read a file:
| read strategy | tokens |
|---|---|
plain whole file (cat) |
2,606 |
numbered whole file (read_file today) |
3,105 |
plain 60-line range (sed -n) |
366 |
numbered 60-line range (read_file with a limit) |
435 |
Two findings fall out, and one dominates. The line numbers cost a real but secondary +19%. The killer is the comparison between rows: a whole-file read costs seven times a targeted 60-line read.
That is the entire story. Codex without my tools reads like a surgeon — rg to locate the symbol, then sed -n '40,80p' to read exactly the forty lines around it. Codex with read_file reads the whole file. The content it pulls into context, not the per-call overhead, is where the tokens go. A narrow numbered read (435 tokens) is nearly as cheap as sed (366); the gap is almost entirely whole-file versus slice. The read-before-write guard makes it slightly worse by adding a mandatory read before every edit — more round-trips, not fewer.
The "shelling out is expensive" intuition had it exactly inverted. Codex's native rg/sed/apply_patch workflow is token-efficient. It reads little, patches precisely, and dumps almost nothing into context. My tools replaced a frugal habit with a profligate one and called it an upgrade.
Trying to close the gap #
The deterministic benchmark also pointed at the fix: if a whole-file read is 7× a narrow one, make Codex read narrow. I lowered the default cap from 2000 lines to 250, added a truncation note so the model knows to page (... [showing lines 1-220 of 400; call read_file again with offset=221 for more]), and rewrote the description to command narrow reads — pass offset and limit, read only the region you need, like sed -n.
It helped, and it was not enough. The excess uncached input roughly halved, from +41% to +24% over native. The head-inside-a-diff verification trick Codex had been using vanished, replaced by small read_file calls. But the tool-call arguments showed why it only went partway: Codex now passes limits and pages with offset, but it reads ~220-line chunks, right up near the cap — not sed's surgical fifty. Even told to read narrow, it reads wide. Pushing the cap lower would force the issue, at the cost of more pagination round-trips, with diminishing returns lost in the noise.
The verdict survived the tuning: still ~12% more expensive per task on a real codebase. The narrowing is a genuine improvement over the untuned version, so I kept it. It does not turn a loss into a win.
What I learned #
The premise was wrong, and only measurement revealed it. "Obviously this saves tokens" was wrong in a way that no amount of design could fix, because the inefficiency lived in the tool's content model, not its plumbing. Build the cheap thing, measure it, and let the measurement overrule the intuition — even when the intuition is the reason you started.
Ask the model why. The most useful instrument in the whole project was a plain-English question to the worker after each run. "I treated edit_file as a preference, not a hard prohibition" told me the fix in one sentence and saved a dozen blind iterations. Counts tell you what the model did; the model itself will tell you why, and the why is what you can act on.
Imperative descriptions beat soft ones, and can outrank the model's own habits. "Prefer X over Y" reads as optional and loses to a system-prompt default. "Use X for EVERY case — do NOT use Y" wins. If you ship an MCP tool that competes with a model's trained-in behavior, name the competitor and forbid it. This is the one reusable, generalizable result, and it has nothing to do with file tools.
Deterministic micro-benchmarks beat noisy end-to-end runs for finding mechanisms. Codex runs swing ±40k tokens; a clean +12% signal hides in that. Measuring the tools' output directly — 190 files, no model in the loop — gave me the 7× number with no noise and no cost. Use the agent to measure adoption; use a script to measure mechanism.
The tool has value — just not the value we expected. A read-before-write safety guard, line-numbered reads the model says help it edit, consistent tooling, and correct worktree resolution are all real. They are worth paying a modest token premium for, if you want them. They are not a way to save tokens. If raw cost or latency is the goal, Codex's native tools already win, and the honest recommendation is to leave them alone.
We shipped a clean, well-tested, fully-adopted MCP server, and the most valuable thing it produced was a number that told us not to use it for the reason we built it. That is a successful experiment with a failed hypothesis, which is the only kind worth writing down.