Rewriting 535,000 lines with AI agents: Bun's method, and what it doesn't prove

Bun went from Zig to Rust in 11 days with 64 Claude agents, for $165,000. The method transfers to a solo project. The four numbers everyone quotes do not.

Claude Code agents orchestration Bun Rust code migration

On August 20, Bun 1.4 shipped. It is the first release of the runtime written in Rust, after four years in Zig. The sentence going around since is a bad summary of what happened: "one guy rewrote a million lines in 11 days with 64 Claude agents for $165,000."

Every number in that sentence is sourced and accurate. The conclusion most people draw from it is not.

Jarred Sumner published a post-mortem of the port on July 8, and it is the most detailed public document that exists today on orchestrating agents at this scale. There are two things to do with it: steal the method, which is solid and transfers to a one-person project, and stop quoting the numbers sideways, because they do not say what people make them say.

What actually happened, in order

DateEvent
December 2, 2025Anthropic acquires Bun
May 3, 2026Zig to Rust port begins
May 13, 2026Bun 1.3.14, the last Zig release
May 14, 2026PR #30412 merges, 11 days in
June 11, 2026Sumner presents the port at Code w/ Claude Tokyo
June 17, 2026Claude Code 2.1.181 ships the Rust Bun in production
July 8, 2026Post-mortem published
July 9, 2026Andrew Kelley, Zig's creator, responds
August 20, 2026Bun 1.4.0, the first public Rust release

One thing to establish up front, because it colors everything else: Bun has belonged to Anthropic since December 2025, and Jarred Sumner works there. The port was done with a pre-release build of Claude Fable 5, which shipped publicly a month later. Claude Code runs on Bun. So this is not a neutral customer case study, it is Anthropic testing an unreleased model on its own infrastructure and publishing the write-up. That invalidates nothing, everything is verifiable. But it means reading the numbers that follow with an eye on who produced them and why.

The method, step by step

Two decisions before the first line of code

Sumner says he settled only two questions up front, and treated everything else as tactics.

All at once or incrementally:

everything all at once is better. An incremental rewrite adds temporary code that you hope gets deleted eventually, and would be painful in the short-medium term.

He decides on the basis of an earlier experience, porting esbuild's transpiler to Zig, done without LLMs. No compatibility layer, no Zig-to-Rust FFI to maintain for six months.

Faithful translation or idiomatic Rust:

Do the rewrite that looks like we transpiled our Zig code to Rust. We can gradually refactor it to reduce unsafe usage and look more like idiomatic Rust after Bun v1.4 ships.

This is the single most consequential call of the project. By keeping the Zig architecture intact, the team can still read its own code after the port, and every Rust file stays comparable line by line to its original. The price: roughly 13,000 uses of the unsafe keyword in the final codebase, about 4% of the code, 78% of them on a single line.

Three hours of conversation, one file

Before any code, three hours discussing Zig-to-Rust pattern mappings with Claude, serialized into a PORTING.md. That is not a prompt, it is a translation spec every agent receives afterwards. The document was later posted to Hacker News and has been a reference ever since.

The file nobody quotes: LIFETIMES.tsv

This is the real prep work, and the most interesting part technically. A dedicated workflow read every struct field in the codebase, traced control flow on the hard cases, proposed a lifetime for every problematic field, ran each proposal past two adversarial reviewers, then serialized the result into a LIFETIMES.tsv. A final review pass hunted for contradictions between PORTING.md and LIFETIMES.tsv.

Translated to your project: before pointing agents at a migration, first produce a map of the constraint that is going to break everywhere. In Zig to Rust, that is reference lifetimes. On your codebase it might be the transaction model, error handling, or date formats. That map is content, not prompting, and content can be reviewed.

The pilot: three files

Before turning agents loose on 1,448 .zig files, a trial run on three. For each: one implementer writes the .rs, two adversarial reviewers check behavioral fidelity and adherence to the guidelines, one fixer applies the notes. We will get to why that pilot structurally could not surface what actually broke.

The loop: 1 implementer, 2 adversarial reviewers, 1 fixer

This is the transferable core of the whole case, and it has nothing to do with scale.

The implementer sees the original Zig file, the porting plan, and its own reasoning. The reviewers see the diff and nothing else. Not the implementer's reasoning, not its justifications. One instruction: assume this code is wrong, find out why. A reviewer never implements, an implementer never reviews.

The context asymmetry is the whole trick. A model that just wrote code and is then asked to review it will defend its reasoning, because that reasoning is still in its context window. The same model, in a fresh window, handed only the diff and told to tear it apart, finds things.

Three real bugs caught that way, all documented in the post-mortem:

  • a use-after-free on the async close of a libuv pipe, fixed with a Box::leak;
  • a negative timestamp truncation, where trunc() rounds toward zero and floor() was needed to keep nanoseconds in the valid range;
  • an unwrap_or() whose argument was eagerly evaluated, so it panicked even when the default was never needed, replaced with unwrap_or_else().

These are precisely the bugs a tired human reviewer waves through at file number 400.

The wall of 16,000 compile errors

Once everything was translated, roughly 16,000 compiler errors remained. They were treated as a work queue rather than a disaster: grouped by crate, cyclic dependencies untangled first, then for each crate a cargo check, errors written to a file, fixed, reviewed by two adversarial agents, applied.

The anti-cheat rule

Two failure modes showed up here. Claude first read "let's get all the crates to compile" as "stub out the functions that do not compile." Then agents started writing long explanatory comments to justify their workarounds. One sentence added to the reviewers ended it:

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong. Fix the code.

Write that one down. It is the best one-line detector of wrong code I have seen, and it works on humans too.

The test order

Smoke tests first (bun --version, linker errors), then subcommands, then the local suite on 100 randomly picked test files sharded by folder, then full CI per platform until green. Zero tests deleted, zero tests skipped.

The real safety net is not the AI, it is the test suite

Here is what the "64 agents" story hides. Before the project started, Bun already had a suite of roughly one million expect() calls across more than 57,000 files, written in TypeScript. Which makes it agnostic to the implementation language: it tests the runtime's behavior, not the Zig code.

That is what makes the port verifiable. Without that suite, adversarial AI review has nothing to anchor to and becomes theater. Gergely Orosz, who met Sumner in San Francisco, lists three preconditions: an engineer who is very motivated and knows the codebase inside out, an extremely robust test suite ("so when the test suite passes, you know it works"), and the willingness to spend a lot on tokens without knowing whether it will work.

Remove any one of the three and the result is not "somewhat worse," it is unusable.

And that net has blind spots, which the 19 regressions map precisely. Almost all of them came from code that is syntactically identical in both languages but semantically different. The best example: Zig's assert is a function, so its argument runs in every build. Rust's debug_assert! is a macro, so the whole expression is erased in release. An insert_stale call feeding the hot reload graph stopped running in production and broke HMR on some React projects, while debug builds kept passing. No test suite executed in debug mode catches that, no matter how many assertions it has.

The four numbers everyone reads wrong

1. "11 days" is the translation, not the project

The 11 days run from May 3 to May 14, meaning up to the merge. Bun users waited until August 20.

The verifiable detail is in the GitHub releases list: Bun shipped a version every two to four weeks (1.3.11 on March 18, 1.3.12 on April 10, 1.3.13 on April 20, 1.3.14 on May 13). Then nothing for 99 days, until 1.4.0. Zero stable releases in between.

Two honest caveats. First, 1.4 carries far more than the port: Bun.WebView, Bun.Image, Bun.markdown, JSON5, JSONL, the Terminal and cron APIs, Node 26.3 compatibility, Windows ARM64. Part of that gap is feature work. Second, the Rust build was running in production for one user by June 17, 34 days after the merge: Claude Code. Anthropic tested on itself before serving anyone else.

Still, the argument that "without AI we would have had to freeze the project for a year" sits awkwardly next to a release train parked for three months. The honest framing is not 11 days versus 365, it is roughly 110 days from first commit to public release, against an estimated year. Still impressive. That is a 3x, not a 33x.

2. "$165,000 versus three engineers for a year"

The comparison comes from Sumner himself:

By hand, I think this would've taken 3 engineers with full context on the codebase about a year, during which time we wouldn't be able to improve Node.js compatibility, fix bugs, fix security issues or implement new features.

That is a retrospective estimate, not a measurement. Nobody ran three engineers at this port in parallel to compare. There is no control group, so there are no demonstrated savings, only plausible ones.

Sumner is in fact more honest than the people quoting him. His very next sentence:

We never would've done that. The realistic alternative was to do nothing and keep fixing the bugs at the top of this post forever.

The real alternative was not three engineers for a year, it was doing nothing. The savings people compute from his sentence compare AI to a project that would never have existed.

And the $165,000 is the API bill alone, at list price, from Anthropic to Anthropic, for a model nobody else could buy at the time. It excludes Sumner's own time, the three months of stabilization, and the cost of the 19 known regressions that reached production, all since fixed. The number is real. It is not complete.

What does hold up: 5.9 billion uncached input tokens, 690 million output tokens, 72 billion cached reads. The ratio between cached and uncached is the actual economic lesson of the run. Without the cache, the bill was not $165,000. If you orchestrate agents in series over one codebase, that ratio is where your bill is decided.

3. "5x less CPU, 35% less memory"

Those are 1.4's numbers, and they are accurate. They do not come from Rust.

The release post explicitly credits the idle CPU drop to optimizing when garbage collector timers request a GC, changing how JavaScriptCore visits Strong roots (from a linked list to a linked list of segmented arrays), reducing futex calls, and the mimalloc changes. The memory drop comes from JavaScriptCore now using mimalloc, extended with partial page clearing, a scavenger thread that frees memory while JavaScript idles, and improved lazy zeroing.

None of that depends on the runtime's language: JavaScriptCore and the allocator are C++, and the same work was doable on the Zig codebase. That is exactly Kelley's complaint about link-time optimization: the gain was available before, it just was not switched on.

The gains genuinely attributable to the port are the ones Sumner measures himself in the post-mortem, and they are modest: 2.8% to 4.8% more HTTP throughput, 2.2% to 4.7% on builds, and Claude Code starting 10% faster on Linux (517ms to 464ms). The real structural win is not a perf curve, it is a class of bug becoming impossible: the use-after-free and double-free bugs that haunted the runtime are now compile errors, and an in-process build leak went from 3,506 MB to 586 MB after a thousand builds.

Sumner sums it up better than his amplifiers do:

Startup got 10% faster on Linux but otherwise, barely anyone noticed. Boring is good.

4. "Nobody reviewed it"

Andrew Kelley, Zig's creator, called the operation "unreviewed slop". It is the most widely repeated criticism of the project, and it deserves to be examined seriously rather than waved away.

Strictly speaking he is right: nobody read a million lines line by line. But that was not hidden, it was the stated and documented protocol. Trust rests on the test suite, not on human eyes.

"Unreviewed" is still a shortcut. Sumner writes that he reviewed the PR by checking that the adversarial agents were correctly catching discrepancies between the Zig and the Rust, that the porting guide and the lifetime guide were being followed, and by reading a lot of the code himself side by side, Zig against Rust. That is sampled, process-level review. It is not exhaustive review, and it is not nothing either.

His sharper objection is elsewhere, and it is hard to dismiss: if the test suite catches everything, how do you explain the memory bugs that lingered in the Zig version? The honest answer is that it does not catch everything. It catches behavioral regressions. Memory bugs are now caught by the Rust compiler. Those are two different nets, and it is their overlap that makes the project defensible, not the AI review.

And we have a measurement of that gap, not just an intuition. On May 14, the day of the merge, an outside contributor opened issue #30719: PathString::slice builds a slice from a raw pointer after the allocation has been freed. Undefined behavior reachable from supposedly safe Rust, with no unsafe visible at the call site. That is precisely the weak spot of a mechanical translation: the pattern was legal in Zig, its literal port to Rust is not. Two days later, PR #30876 added cargo-miri support and fixed a HiveArray aliasing problem along the way. By August, Miri runs in CI across several crates (#37078, #39218), and Bun publishes a public audit of its unsafe code.

Read that sequence both ways. The project reacted in two days, which counts in its favor. But neither the million assertions nor the adversarial reviewers had caught it: it took a verification tool the project had not wired up yet, and an outsider to ask for it. The net had a hole, exactly the size Kelley said it would.

Kelley goes further, arguing Bun's real problems were organizational rather than linguistic:

The main problem, however, was code quality.

He describes hacks stacked on hacks, abuse of assertions, and a headlong rush from feature to feature. On the point that some of 1.4's gains were reachable in Zig, he is factually right. On the rest, he is comparing a project he would have run differently to a project someone else ran, which is not refutable either.

The two process mistakes you can skip

The three-file pilot tested the wrong variable

Three samples is too few to conclude anything. But the sample size is not the real problem.

The pilot validated translation fidelity, file by file, in series, with one implementer and two reviewers. What actually broke afterwards was the behavior of several agents running in parallel against the same git repository. Even fifty files in series would not have surfaced it: that is not a volume problem, it is a concurrency problem.

Going from 3 files in series to 1,448 files across 64 concurrent agents changes two variables at once. If you pilot an agent-driven migration, your trial has to include the target execution mode, not just a slice of the work. Three files across two parallel worktrees would have been worth more than ten files in series.

Agents stepping on each other was predictable

The post-mortem is candid about it. Two minutes into the run across 1,448 files, one Claude ran git stash before committing, another ran git stash pop, then a git reset HEAD --hard. They were stepping on each other. The rules added: never git stash, never git reset, no git command that does not commit one specific file, no cargo, no slow commands at all. Then a split into 4 shards, 4 worktrees, 16 agents each, each committing only its own files.

In fairness, he explains why every agent did not get its own worktree from the start: Bun's repository is too large for 64 copies on disk, and the changes have to compile together anyway. The constraint is real.

That does not excuse the rest. Multiple autonomous processes writing to shared state with no lock is a textbook race condition. Those rules belonged in the system prompt before the first run, not two minutes into it. The actual cost was two minutes and some cleanup, but only because someone was watching the screen.

Good news for you: you no longer have to reinvent that guardrail. Claude Code runs agents in separate worktrees natively, and that isolation was made genuinely airtight in early August, including against destructive git commands issued from an isolated agent.

What to steal for your own project, even solo

None of this needs 64 agents.

  1. Strictly separate who writes from who attacks. One subagent implements, another reviews in a clean window, with the diff alone and instructions to assume it is wrong. Never hand it the implementer's reasoning.
  2. Write your constraint map before launching anything. Your equivalent of PORTING.md and LIFETIMES.tsv. Versioned, reviewable content, not a prompt.
  3. Translate mechanically, refactor after. A migration that changes architecture and language at the same time is no longer diff-verifiable.
  4. Treat the test net as a prerequisite, not a deliverable. If your suite cannot return a verdict, you do not have a project, you have a bet. That deserves its own read.
  5. Treat compile errors as a work queue, grouped by module, not as an incident.
  6. When it breaks, fix the process that generates the code, not the code. That is Sumner's phrasing: "fixing the process that generates the code instead of hand-fixing the code." A hand patch on one file does not protect the other 1,447.
  7. Put the comment rule in your CLAUDE.md. If a workaround needs a paragraph to justify itself, the code is wrong.
  8. Impose the git rules from the first run, or use native worktree isolation.

What this case is missing to be proof

The hole, and it is the one that bothers me most: nowhere is there a comparison between the code the AI produced and the code a human would have produced on the same files. Not an excerpt, not a control file, nothing.

What we have is new-Rust versus old-Zig metrics. That is not AI-Rust versus hand-written-Rust. The manual port never existed, so this case says exactly nothing about "AI does it better than a human." It says that a mechanical port, backed by an exhaustive test suite that is independent of the implementation language, is doable by agents: 11 days of translation, roughly 110 days to production.

That is already a big deal. It is not the same claim.

And I suspect that hole is going to become the norm, because producing the control costs precisely what you were trying to save. Three files ported by hand, published next to their AI version, would have settled this for everyone. Nobody is going to do it.

Further reading

What to take away

The Bun case is the best public documentation that exists on large-scale agent orchestration, and the implementer plus adversarial reviewers pattern, in separate contexts, transfers as-is to a one-person project.

Use it to talk about method. Never use it as proof that agents produce trustworthy code: without a million-assertion test suite written in a third language, this project does not exist.

As for the 11 days and the $165,000, they fit in one sentence once set straight: that is the time to merge, the public release landed 99 days later, and the three-engineers-for-a-year comparison is the author's estimate, not a measurement.

Pierre Rondeau

Pierre Rondeau

Developer and indie builder. I build products and automations with AI. Creator of Claude Hub.

LinkedIn