Bun’s creator took a 535,496-line Zig codebase and rewrote the whole thing in Rust in 11 days, not by hand but by orchestrating dozens of Claude agents into self-correcting loops. The interesting part is not the destination language; it is the method that made a “never do a rewrite” project tractable.

The bug class that forced the decision

The trigger was not a dislike of Zig but a recurring family of stability bugs that no amount of care seemed to eliminate. Bun mixes a garbage-collected world (JavaScriptCore, with strict rules about exceptions and stack scanning) with manually-managed native memory, and Zig deliberately has no destructors or Drop: cleanup is spelled out explicitly at each call site with defer. That combination produced a steady stream of use-after-free, double-free, and “forgot to free on the error path” bugs. The v1.3.14 changelog alone is a wall of them: heap-use-after-free in node:zlib .reset(), re-entrant callback crashes in node:http2, an ArrayBuffer detach race in UDPSocket.send(), a ~6.5 KB-per-call SSL_SESSION leak in tlsSocket.setSession(), and more.

Sumner is explicit that this was not Zig’s fault. Zig made Bun possible; the initial version was one person, one year, one cramped Oakland apartment, pre-LLM. The problem is that mixing GC with manual memory is rare enough that no language really designs for it, and Bun’s team was already doing more than most to contain the fallout: a patched Zig compiler with Address Sanitizer on every commit, ReleaseSafe builds on Windows, 24/7 Fuzzilli fuzzing, and extensive end-to-end leak tests. The realization was that all of those catch bugs late - fuzzing after merge, CI on push, ASAN at runtime - whereas the whole class could instead be turned into compiler errors caught before anything runs.

Compiler errors beat style guides

The obvious cheaper fixes were rejected for the same reason. A rigid ownership style guide (in the spirit of TigerBeetle’s TigerStyle or Google’s 31,000-word C++ guide) only works if it is enforced, and enforcement means best-effort code review plus linters. Homegrown smart pointers in Zig would buy worse ergonomics than Rust with none of the guarantees, forcing verbose a_ptr.get() / defer a_ptr.deref() boilerplate at every call site. C++ was a genuine option - about 20% of Bun is already C++, and it would bring real constructors and destructors and let them delete piles of extern "C" glue - but it would still lean on style guides and code review, and memory corruption would still happen even with ASAN.

Rust’s pitch was different in kind. The dominant bugs in that changelog (use-after-free, double-free, missing frees on error paths) are simply compiler errors in safe Rust, with automatic Drop-based cleanup replacing manual defer. A compiler error is a strictly better feedback loop than a style guide because it fails closed and it fails early. That is the whole argument for the language switch.

What made a full rewrite feasible

Rewrites are famously a terrible idea, and Sumner agrees: by hand this would have been three engineers with full context working for a year, with all bugfixes, security fixes, and features frozen for that year. That option was never real. Two structural facts changed the math.

First, Bun’s own test suite is written in TypeScript, so it is independent of the runtime’s implementation language. The exact same suite that validated the Zig build could validate the Rust build unchanged - over a million expect() assertions acting as a behavioral spec. Second, the plan was deliberately unambitious about idiomatic Rust: do a mechanical port that looks like the Zig was transpiled, keeping the same architecture, performance, and feature set, and defer the refactor toward idiomatic Rust and reduced unsafe until after v1.4 ships. “Everything all at once” over an incremental port, because incremental means temporary scaffolding code you hope to delete later. Those two decisions turned an open-ended research project into a bounded, checkable one: make the same tests pass, with the smallest behavioral delta possible.

Orchestrating an army of agents

This is the heart of the piece. Sumner did not prompt “rewrite Bun in Rust, don’t make mistakes” and pray. He modeled everyday engineering as a loop - pop a task, produce a result, have reviewers critique it, apply the feedback - and ran roughly 50 such dynamic Claude Code workflows continuously over 11 days. The workflows mapped cleanly onto phases: generate a Zig-to-Rust porting guide, mechanically port every .zig to .rs, fix every crate’s compiler errors, get subcommands like bun test and bun build working, get the whole suite green, then refactor and clean up. At peak, four workflows ran at once, each in its own git worktree, each running 16 Claudes: about 64 Claudes at a time, producing roughly 1,300 lines of code per minute.

The load-bearing technique is adversarial review with split context windows. With humans, the reviewer is deliberately not the author, because an author is biased toward merging. Claude behaves the same way: the Claude that wrote the code wants it accepted, so a separate Claude, in a separate context window, is tasked only with exhaustively arguing why the change is buggy or wrong. The ratio was one implementer to two or more adversarial reviewers, with a hard role split: the implementer never reviews, the reviewer never implements, and a distinct fixer applies the accepted feedback before commit. Every single line went through two independent adversarial reviewers and a round of fixes before being committed, even though none of it compiled or ran yet.

The prep work followed the same de-risking discipline. Three hours of conversation with Claude produced PORTING.md (the pattern mapping, which itself reached Hacker News). A dedicated workflow then traced the control flow of every struct field to propose a Rust lifetime for each, ran two adversarial reviewers over each proposal, and serialized the results into a LIFETIMES.tsv for other Claudes to consult - followed by a joint adversarial review pass over PORTING.md and LIFETIMES.tsv to reconcile conflicts, plus a manual read-through. Only then a trial run on 3 files before unleashing the workflow on all 1,448.

Fix the process, not the code

The defining rule of the whole effort: when output went wrong, Sumner edited the workflow prompt rather than hand-patching the code. The article is candid about the false starts, and each one is a process fix.

The first attempt on all 1,448 files self-destructed within two minutes: parallel Claudes ran git stash, git stash pop, and git reset HEAD --hard on each other’s work, and per-Claude worktrees were not viable because the repo is too large to fork 64 times on disk. The fix was a prompt rule - never run git stash, git reset, any git command that does not commit a specific file, no cargo, no slow commands - plus sharding into 4 worktrees of 16 Claudes each. When the compiler-error phase began, Claude read “get the crates to compile” as license to stub out failing functions and paper over them with paragraph-long justifying comments; the fix was a reviewer rule: “If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong - fix the code.” One prompt edit later, it stopped. Heavy integration and leak tests (a next dev HMR test, tests spawning ~10k processes, tests writing gigabytes) needed more than “please,” so isolation moved to systemd-run cgroups and pid namespaces.

Compiler errors became the work queue. Splitting the single Zig compilation unit into ~100 Rust crates (for faster compiles) surfaced cyclical dependencies, which Sumner resolved with one workflow to classify where cyclic code belonged and another to do the refactor. That exposed roughly 16,000 compiler errors - “a massive number for 1 human, but not a crazy number for 64 claudes at once.” The queue-draining loop ran per crate: cargo check once at the start, group errors by file, one Claude fixes the crate, two adversarial reviewers, one fixer applies, and no git until the very end so the Claudes never collided. The same shape recurred at every later stage - failing stacktraces per subcommand, then ~100 random test files per shard - each failure written to a file and fed back into implementer-then-reviewers-then-fixer. Two days after the first CI run the Linux failing list dropped from 972 test files to 23, and a day and a half later Linux went fully green.

What the port bought

The economics: pre-merge it consumed 5.9 billion uncached input tokens, 690 million output tokens, and 72 billion cached input reads, about $165,000 at API pricing, across 6,778 commits (May 3 to May 14), using a pre-release Claude Fable 5. Zero tests were skipped or deleted, and all three platforms ended with ~1.0-1.4 million expect() calls passing. The initial PR added over a million lines.

The results validate the compiler-error thesis. Bun v1.4.0 fixes 128 bugs that reproduce in v1.3.14, and only 19 known regressions were introduced (all since fixed), most from code that is syntactically identical but semantically different between the languages: a side effect erased inside Rust’s debug_assert! macro (which broke HMR), bytemuck::cast_slice panicking on an odd-length slice, retained bounds checks turning a ported off-by-one into a panic, and comptime format strings that no longer resolved at compile time. Drop eliminated leaks that a prior Zig attempt could not confidently merge: repeated Bun.build() went from leaking ~3 MB per build (6,745 MB after 2,000 builds) to leveling off at ~609 MB. Binaries shrank ~20% on Linux and Windows, recursive-descent parsers use less stack, and HTTP throughput and CLI workloads run 2-5% faster thanks to cross-language LTO. Prisma runs it in production, and Claude Code has shipped on the Rust port since v2.1.181 - “barely anyone noticed. Boring is good.”

Key takeaways

  • Switch languages to convert an entire class of runtime bugs into compiler errors; that early, fail-closed feedback loop is the real argument for Rust over a style guide or C++.
  • A language-independent test suite (Bun’s was in TypeScript, 1M+ assertions) is what makes a full rewrite checkable rather than reckless - it is the behavioral spec the port must satisfy.
  • Aim for a mechanical, transpile-shaped port with minimal behavioral change, “everything all at once”; defer idiomatic refactoring until after shipping.
  • Separate implementer and reviewer into distinct context windows: the author-Claude is biased to merge, so a dedicated adversarial reviewer-Claude (2+ per implementer) must only hunt for reasons the code is wrong.
  • When output is bad, fix the prompt/process, not the code - encode failures as reusable rules (no destructive git, reject workarounds that need paragraph-long comments).
  • Treat compiler errors and failing stacktraces as a parallelizable work queue; 16,000 errors is intractable for one human and routine for 64 agents.
  • Watch for syntactically-identical, semantically-different constructs across languages (macro vs function, retained vs stripped bounds checks, comptime vs runtime) - that is where regressions hide.
  • The multiplier is real: one engineer closely monitoring orchestrated agents did in 11 days what was estimated at three engineers for a year.

Sources