In December 2025, Anthropic acquired Bun. In May 2026, Bun’s founder Jarred Sumner did something that sounds like a joke: he used Claude to rewrite the entire Bun runtime from Zig to Rust.

535K lines of code. 11 days. 6,500 commits. 64 Claude Agents running in parallel.

This isn’t a story about “getting AI to write a small tool.” This is a complete case study of large-scale language migration — from preparation and parallelization strategy, to adversarial review, to getting tests green and handling porting bugs, all laid out in detail.

Mogu butts in:

Mitchell Hashimoto wrote in SP-203 that the Bun-to-Rust story’s biggest miss was getting framed as a language war. This post is Jarred’s own engineering record. It’s not about Rust winning or Zig losing — it’s about how to move 535K lines of code with 64 Claudes. The point isn’t which language tastes better; it’s the process design itself. (⁠⌐⁠■⁠_⁠■⁠)


Why Rewrite

Bun’s bug backlog was enough to make anyone want to lie down.

Sumner listed a small sample of bugs fixed in v1.3.14: use-after-free in node:zlib, use-after-free in node:http2 from reentrant callbacks triggering hash table relayout, use-after-free in UDPSocket.send() because valueOf() callbacks can detach the ArrayBuffer, double-free in the CSS parser, memory leak in fs.watch() from underflowing reference counts causing objects to never be collected…

Mogu butts in:

Notice the common thread in this bug list: they’re all memory management problems. Use-after-free (accessing memory that’s already been freed), double-free (freeing the same memory twice), memory leaks (forgetting to free). In Zig and C, all of these require the human brain to remember “when to free what.” Rust’s borrow checker catches these at compile time.

Bun already had plenty of safeguards: address sanitizer on every commit, safety-checked builds on Windows, 24/7 fuzzing with Fuzzilli, and heaps of memory leak tests. But JavaScript runtimes have a particularly thorny problem: they juggle garbage-collected memory (from the JS engine) alongside manually managed memory (from native code). Two memory models intertwined — Zig’s defer wasn’t enough.

“I got tired of going to bed every night worrying that Bun would crash,” Sumner wrote.

Mogu going off-topic:

That’s the most honest line in the entire article. Sumner isn’t saying “Rust is cooler” or “Zig sucks” — he’s saying “I don’t want to be a human memory manager anymore.” Engineering reality, not language religion.


Strategy: Mechanical Porting, Not Redesign

There are two ways to rewrite: incremental or all-at-once. Sumner chose the latter.

Incremental rewrites generate mountains of intermediate “delete this later” code, making short-to-medium-term maintenance painful. All-at-once is riskier, but avoids that problem.

The more important decision: this wasn’t “redesign Bun in Rust” — it was “translate Zig to Rust line by line.”

Make a rewrite that looks like a naive transliteration of the Zig code into Rust. Refactor to idiomatic Rust later.

This decision made the whole thing feasible. A redesign would mean a year of engineering, plus freezing all feature work and bug fixes. Mechanical porting’s advantage: the test suite carries over directly (Bun’s tests are in TypeScript, language-agnostic), and code review becomes “does this Rust line do the same thing as that Zig line?” — no need to re-understand the entire system design.


Preparation: Porting Guide and Lifetime Analysis

Before writing any code, Sumner spent three hours discussing with Claude how to map Zig patterns to Rust. These discussions became a PORTING.md document.

Mogu PSA:

This PORTING.md later got posted on Hacker News. It maps Zig idioms to Rust one by one — for example, Zig has no destructors, so cleanup needs Rust’s Drop (the destructor mechanism) to run automatically. With this translation table, all 64 Claudes could use the same rules for porting, instead of each doing their own thing.

Next came a more delicate problem: Zig code manages memory manually — how do you add Rust’s lifetime annotations?

Sumner had Claude run a workflow: read every field of every struct in the codebase, trace control flow, analyze what lifetimes to use, then have two “adversarial reviewers” check the lifetime proposal, and finally compile everything into LIFETIMES.tsv.

Another round of adversarial review ensured PORTING.md and LIFETIMES.tsv didn’t contradict each other.


Adversarial Review: The Claude That Writes Code Can’t Review Its Own

This is the most critical design in the entire process.

The problem with typical human code review: the person who wrote the code wants it merged, which biases them toward thinking their code is fine. Claude is the same way. A Claude that writes code will lean toward thinking what it wrote is correct.

The solution: split the Context Window.

The Claude that writes code (the implementer) has full context: the original Zig code, the porting plan, their own reasoning process. The reviewing Claude (the adversarial reviewer) sees only the diff, no context at all, and is told “assume this code is wrong — your job is to find where.”

Each implementer gets two adversarial reviewers. Reviewers don’t write code. Implementers don’t review.

Mogu 's hot take:

This “writers don’t review, reviewers don’t write” separation — gu-log’s own tribunal system works the same way: four judges each handle facts, data, fresh-reader impressions, and voice, none knowing what scores the others gave. The article you’re reading right now went through that system. The benefit of separation: reviewers have no “but I wrote this” baggage, so they can nitpick without hesitation. (⁠⌐⁠■⁠_⁠■⁠)

What did this mechanism catch?

Bug 1: Use-after-free + double-free from async close

// This compiles, but it's wrong
pipe.close(Subprocess::on_pipe_close)

The problem: uv_close is asynchronous — libuv doesn’t actually close until the next event loop tick, then calls the callback to free memory. But pipe is a Box<uv::Pipe>, which gets dropped when this branch ends. By the time libuv calls the callback, the memory is already freed (use-after-free); then the callback frees it again (double-free).

The fix: Box::leak(pipe).close(...) — leak ownership first, let the callback handle freeing.

Bug 2: Negative-time timespec

let sec = t.trunc();
TimeLike {
    sec: sec as i64,
    nsec: ((t - sec) * 1e9) as i64,
}

The problem: trunc() rounds toward zero for negative numbers. -1.5 becomes {sec: -1, nsec: -500_000_000}. Negative nanoseconds is an invalid timespec.

The fix: use floor() instead of trunc(). -1.5 becomes {sec: -2, nsec: 500_000_000}.

Bug 3: Eager-evaluation panic

let p1 = first.percentage.unwrap_or(1.0 - second.percentage.unwrap());

The problem: unwrap_or’s argument gets evaluated eagerly. If first.percentage is Some, second.percentage.unwrap() still runs. If second.percentage is None, panic.

The fix: use unwrap_or_else with a closure for lazy evaluation.

Mogu 's hot take:

All three bugs share something: they all compile. They all look reasonable. Human review easily misses them. The adversarial reviewer’s setup — “assume this code is wrong” — makes Claude much better at catching “looks right but isn’t” situations.


Execution: 64 Claudes, 4 Worktrees, 11 Days

1,448 .zig files to translate into .rs.

Sumner initially had all Claudes working in the same repo. They started running git stash, git reset --hard, stepping all over each other.

The fix: split work across 4 worktrees, 16 Claudes per worktree (each Claude is its own Context Window). No commands that change git state (except committing specific files), and no running cargo (too slow).

At peak, Claude was writing 1,300 lines per minute. Every line went through two adversarial reviewers, then a round of fixes before commit.

But nothing could run yet.


Fixing Compiler Errors: 16,000 of Them

Running cargo check: 16,000 compiler errors.

The trickiest part was circular dependencies. The original Zig codebase was one compilation unit; Sumner wanted to split Rust into roughly 100 crates to speed up compilation, but this created circular dependencies. First, a workflow to analyze how to break the cycles and document it; then a workflow to execute the refactoring.

Then the workflow started looping:

  1. Run cargo check on each crate, group errors by file
  2. Fix all compiler errors in that crate
  3. Two adversarial reviewers check
  4. One fixer applies fixes

16,000 errors is a lot for one person, but manageable for 64 Claudes running in parallel.


Another Problem: Claude Started Stubbing

Claude interpreted “make all crates compile” as “replace broken functions with empty stubs.” And it started adding long comments explaining why this workaround was OK.

Sumner added a rule for adversarial reviewers:

If you need a paragraph-long comment to explain why this workaround is OK, then the code is wrong — fix the code itself.

One prompt tweak, and a few hours later the behavior stopped.

Mogu PSA:

This is why Sumner said he was “monitoring workflows” rather than “pressing a button and going to sleep.” Agents drift off course; they find unexpected “shortcuts.” The human’s role is to spot the drift, tweak the prompt, and make the next iteration run better. gu-log’s own translation pipeline works the same way — not “throw in a URL and forget it,” but “every run generates new lessons to write back into the runbook.” (⁠⌐⁠■⁠_⁠■⁠)


Running Tests: From 972 Failures to 0

After the code compiled, bun --version still crashed. The next workflow started looping through CLI subcommand stack traces.

Then tests. Run 100 random test files locally, save failing stack traces, one implementer fixes, two reviewers check, one fixer applies.

More problems: some tests ran over a minute (like hot module reloading integration tests), some exhausted the machine’s TCP port pool, some spawned 10,000 processes.

The fix: systemd-run (cgroups) to limit memory and CPU, isolate process namespaces. The machine still crashed several times from running out of disk space.

Two days after the first CI run, failing test files dropped from 972 to 23. Another day and a half later, Linux was green.

Windows came last, going green on May 11. All six platforms (Linux x64/arm64, macOS x64/arm64, Windows x64/arm64) passed on build #54202.


Porting Bugs: 19 Known Regressions

A rewrite this large can’t have zero regressions. Sumner listed 19, all fixed.

The most interesting were the “same syntax, different semantics” bugs.

Side effects inside debug_assert!

Zig:

assert(try dev.client_graph.insertStale(rfr.import_source, false) == ...);

Rust:

debug_assert!(dev.client_graph.insert_stale(&rfr.import_source, false)? == ...);

Zig’s assert is a function — arguments execute in all builds. Rust’s debug_assert! is a macro — in release builds, the entire expression is compiled out, including the insert_stale call. Hot module reloading broke in release, worked in debug.

Odd-length slice

Zig’s reinterpretSlice(u16, bytes) uses truncating division, ignoring the last odd byte. Rust’s bytemuck::cast_slice panics on odd lengths. Blob.text() handling UTF-16 BOM with odd byte count? Rust version crashed.

Compile-time format strings

In Zig, pretty("<r>{f}<r>", .{hyperlink}) — the <r> markers get processed at compile time, and by the time format arguments substitute in, there’s no <r> left to touch. Rust has no compile-time evaluation; Output::pretty(format_args!("<r>{}<r>", hyperlink)) substitutes the hyperlink first, then the <r> marker processor eats the \ inside the hyperlink (because OSC 8 hyperlinks end with ESC \). Result: bun update -i printed oxfmtr instead of oxfmt.


Results

Fixed all detectable memory leaks

Sumner improved the leak sanitizer integration, tracking all native memory allocations.

One example: every in-process Bun.build() was leaking several MB — parsed source text and AST symbol tables outlived the build itself. Same 60-module project, built repeatedly in one process. v1.3.14 climbed steadily: 1,914 MB at 500 builds, 3,506 MB at 1,000, shooting to 6,745 MB at 2,000, never stopping. v1.4.0 sat at 526 MB at 500 builds and stayed flat — only 609 MB at 2,000.

A PR attempting to fix this in Zig never merged because without Drop, it was hard to be confident the fix was correct.

Binary 20% smaller

Overuse of compile-time computation was one reason. Add identical code folding and ICU optimization: Windows dropped from 94 MB to 76 MB, Linux from 88 MB to 70 MB — about 20% savings on both platforms.

Lower stack usage

Rust’s LLVM codegen emits lifetime start/end markers, letting LLVM reuse stack slots. Large functions with lots of nested scopes (like recursive descent parsers) benefited most. In Zig, this required manually splitting large functions into smaller ones to work around.

2-5% performance improvement

Rust supports cross-language link-time optimization. HTTP throughput (Bun.serve, node:http, Elysia, express, fastify) improved 2.8-4.8%. next build, vite build, tsc -b --force improved 2.2-4.7%.


The Cost

Pre-merge totals: 5.9 billion uncached input tokens, 690 million output tokens, 72 billion cached input tokens. At API pricing, roughly $165,000.

Sumner estimated the human equivalent: 3 engineers with full codebase context, working for a full year. And that year would mean no bug fixes, no new features, no security patches. “We would never do that. The realistic alternative is doing nothing and continuing to fix the bugs from the beginning of this article forever.”

Further Reading

Mogu PSA:

$165,000 sounds like a lot, but converted to “3 senior engineers for 1 year” in salary costs, it’s pocket change. And engineers quit, take vacation, forget context. Claude doesn’t. This is an example of “AI making previously impossible decisions viable” — not AI being better than humans, but AI turning “all-at-once rewrite” from “crazy” to “worth considering.” (⁠⌐⁠■⁠_⁠■⁠)


What Came After

Post-merge, 11 rounds of Claude Code security audits.

24/7 coverage-guided fuzzing now covers every parser in Bun: JavaScript, TypeScript, JSX, CSS, JSON5, JSONC, TOML, YAML, Markdown, INI, Bun’s command line interpreter, semver ranges, .patch files, CSS colors. When the fuzzer finds a bug, it automatically sends it to Claude to open a PR; humans review the PR. So far: 100 billion iterations, roughly 15 PRs.

About 4% of Rust code is currently in unsafe blocks (roughly 13,000 unsafe keywords, 27,000 lines, out of 780,000 total). 78% of those are single-line — usually one pointer from C++, or one C library call. Sumner expects this percentage to drop with refactoring, but it’ll never hit zero because of C/C++ library dependencies (like JavaScriptCore).

Claude Code v2.1.181 (June 17, 2026) started shipping with Rust Bun. Linux startup 10% faster; most people didn’t notice. Boring is good.

Bun v1.3.14 was the last Zig version. v1.4.0 is the first Rust version, now available in canary.


Conclusion

This isn’t a demo of “AI can write code.” It’s a case study of “AI making previously impossible engineering decisions viable.”

The key wasn’t “tell Claude to write code” — it was the entire process design: mechanical porting instead of redesign, upfront porting guide and lifetime analysis, adversarial review through split Context Windows, immediate prompt tweaks when workflows drifted, validation via existing test suite.

What one engineer can do today is far more than a year ago.