How Bun Got Ported to Rust — and What It Teaches Us About Working with Agents

After the agent reported background command "Build Bun debug binary" completed (exit code 0) and I had a Bun binary compiled from Rust, I handed Claude the next task: analyze this PR and write a blog post on how Jarred Sumner pulled off the Zig-to-Rust port — so the rest of us can learn to work better with agents. What follows is what came back, possibly with traces of the Mythos model.

On May 14, 2026, Jarred Sumner merged PR #30412 — "Rewrite Bun in Rust" into oven-sh/bun. It was one of the largest single PRs in the history of public open source: 2,188 files changed, 1,009,257 lines added. It reimplemented an entire JavaScript runtime — bundler, package manager, HTTP/2/3 stack, dev server, transpiler, the lot — from Zig to Rust, while keeping the same architecture and the same test suite passing.

It almost certainly couldn't have happened without Claude agents. The PR doesn't say so out loud — the body just says "compiler-assisted tools for catching & preventing memory bugs" — but the commit history leaks the orchestration. Ten minutes after the merge, Jarred pushed another commit titled "Delete stray files" that removed 34 files from .claude/workflows/. Those files, briefly public in git history, tell the story.

This post is about what we can learn from that story — not about Bun specifically, but about how to set up a codebase, instructions, and tooling so that agents can do work humans alone couldn't ship.

The leaked evidence🔗

The deleted directory contained workflow scripts named in a deliberate phase order:

phase-a-port.workflow.js
phase-b0-cyclebreak / movein / moveout / verify.workflow.js
phase-b1-tier.workflow.js
phase-b2-cycle / fill / fix-bugs / keystone / ungate-tier / verify.workflow.js
phase-c-panic-swarm.workflow.js
phase-d-build-queue / crate-shard / todo-sweep / unsafe-audit /
       blocked-on-resolve / bundler-perfile / bundler-shard /
       recursive-ungate / subtree-batch.workflow.js
phase-e-body-port / mass-ungate / proper-port / test-bringup /
       scopeguard-sweep.workflow.js
phase-f-probe-swarm / test-swarm.workflow.js
phase-g-mega-swarm / test-swarm.workflow.js
phase-h-ci-tasks / dedup / deep-dive / diff-review / idioms-audit /
       libuv-audit / main-parity / unsafe-wrap.workflow.js
lifetime-classify.workflow.js

And one more file, .apply-lock-guard.sh:

#!/bin/bash
# Serialize Apply-phase agents on /root/bun-5 working tree.
# Usage: flock /root/bun-5/.apply-lock bash .apply-lock-guard.sh -- <command...>
exec "$@"

A four-line flock wrapper to serialize parallel agents writing to a shared working tree at /root/bun-5. Whoever was running this had many agents simultaneously editing the same checkout and used filesystem locks to keep them from colliding.

The body of phase-a-port.workflow.js is more telling:

export const meta = {
  name: "phase-a-port",
  description: "Phase A: draft .rs for a batch of .zig files (implement → verify → fix)",
  phases: [
    { title: "Implement", detail: "one agent per .zig writes draft .rs per PORTING.md" },
    { title: "Verify",    detail: "adversarial check of .rs against .zig + PORTING.md rules" },
    { title: "Fix",       detail: "apply verifier findings to .rs" },
  ],
};

One agent per Zig file. A second adversarial agent verifies against a PORTING.md style guide. A third applies the verifier's findings. All outputs are JSON-Schema-validated (IMPL_SCHEMA, VERIFY_SCHEMA are defined further down) so the next phase can consume them mechanically.

What's still visible in the tree🔗

The workflows got deleted, but their fingerprints are everywhere:

  • 1,292 .zig files retained alongside 1,432 .rs files. The root CLAUDE.md is explicit (line 141): "These are the original Zig implementation, kept only as a porting reference — they are not compiled and not shipped. New code goes in .rs. When fixing a bug or porting a behavior, the .zig sibling is the source of truth for intended semantics."

  • 10,841 PORT NOTE: / PERF(port): / TODO(port): comments across the Rust source. That's an average of seven to eight per file. They aren't decorative. Look at src/bundler/defines.rs:

    // PORT NOTE: Zig pre-counted `key_buf_len`/`e_strings_to_allocate` to size
    // two bump allocations, then `iter.reset()` and re-walked. With per-entry
    // copies the pre-sizing pass is dead — emit directly.
    // PERF(port): was single-buffer key arena; now per-entry Vec reuse.
    

    Each annotation explains why the Rust diverges from the Zig — for the next agent that reads the file.

  • 108 Cargo crates in a single workspace, prefixed bun_core, bun_sys, bun_paths, bun_jsc, bun_runtime, bun_js_parser, bun_js_printer, bun_bundler, bun_install, … Tiny crates aren't just an architectural choice — they're a parallelism boundary. Each crate is a sharded work unit an agent can own, check, and finish independently with cargo check -p <crate>.

  • Hard-coded branch naming. CLAUDE.md line 266: "Branch names must start with claude/. This is a requirement for the CI to work." The CI distinguishes agent commits from human ones.

  • A prescriptive style guide masquerading as developer docs. src/CLAUDE.md (14 KB) contains a six-column table telling you which std:: APIs are forbidden and what to use instead — bun_sys::File over std::fs::File, bun_paths::dirname over Path::parent, bun_core::env_var::*::get() over std::env::var, and so on. No discovery, no judgment. The agents don't get to "find a Rusty equivalent." They get told.

  • A "code review self-check" section in the same file:

    Before writing code that makes a non-obvious choice, pre-emptively ask "why this and not the alternative?" If you can't answer, research until you can — don't write first and justify later.

    And, bluntly, in the Important Notes section:

    Be humble & honest — NEVER overstate what you got done or what actually works in commits, PRs or in messages to the user.

That last one only exists because someone — many someones — were overstating.

The phases as a thinking pattern🔗

Read the phase names as a sentence, not a list:

  • A — implement a first draft (one agent per file)
  • B0–B2 — break dependency cycles, move types between crates, tier them so each crate compiles
  • C — sweep panics out of the codebase ("panic-swarm")
  • D — fan out per-crate, per-subtree, per-bundler-file work; sweep TODOs and audit unsafe
  • E — port real function bodies on top of the now-compiling skeleton
  • F–G — swarm probes and tests
  • H — diff review, dedup, idiom audit, libuv parity, unsafe wrap-up, CI cleanup

The shape is a draft, then a structural pass to make it compile, then bodies, then tests, then audits. Each phase has different invariants, different inputs, different verifier prompts. A single "port this codebase" mega-prompt would have produced slop — broken down this way, each phase becomes a small enough problem to give to an agent with a tight contract.

Portable lessons🔗

If you take one thing from this, take this: the workflow scripts leaked because the agent infrastructure was project-specific, not generic. Jarred didn't run "Claude, port my repo." He built a porting harness for his repo, with his conventions, his crate layout, and his style guide, and then ran agents through it. The model is the same one anyone can use; the leverage came from the harness around it.

Specific patterns worth stealing:

1. Keep the old code in-tree as ground truth. Don't delete the Zig source the day you check in the Rust. Agents lose track of intent between sessions; the original is the only stable reference for "what is this supposed to do." 1,292 .zig files weren't dead weight — they were the spec.

2. Annotate every divergence with WHY, not WHAT. PORT NOTE: reshaped for borrowck — drop deflate borrow before re-borrowing self. The next agent doesn't need to be told that Rust has a borrow checker. It needs to be told that this specific line exists because a previous agent already fought through the obvious naive version and that version didn't work. Without this, every agent re-discovers and re-litigates the same workarounds.

3. Write a prescriptive style guide, not an aspirational one. If you have an opinion, encode it as a forbidden/replacement table. Agents are excellent rule-followers and terrible taste-havers. Don't let them choose between std::fs::File and your wrapper — say you must use the wrapper, here is the wrapper, here is when to call which method.

4. Split implement from verify from apply. The phase-A workflow had three roles per file: drafter, adversarial reviewer, fixer. The adversarial agent can't see the drafter's reasoning, only its output and the rules. This forces the drafter to be explicit (because the reviewer will catch implicit assumptions) and stops the kind of self-validating drift that single-agent loops produce.

5. Demand structured outputs between phases. IMPL_SCHEMA and VERIFY_SCHEMA weren't optional — the workflow validates them. When agents hand off to other agents, free-form prose loses information. Make them fill in a JSON object with rs_path, confidence, todos, rs_loc, note. The next phase parses that, doesn't re-read essays.

6. Shard the work for parallelism, lock the writes. ~200 small crates make parallelism cheap because the cargo check granularity matches the agent granularity. A flock guard around the working tree makes the parallelism safe. Both are needed — the architecture and the mechanism.

7. Tag agent work in CI. claude/-prefixed branches give you a way to gate, count, and roll back agent-produced changes separately from human ones. This is the boring kind of infrastructure that becomes essential the moment your agent volume exceeds your review volume.

8. Write rules to fight confabulation explicitly. "NEVER overstate what you got done." "Pre-emptively ask why this and not the alternative." These read like leadership advice, but in context they're load-bearing — they exist because the failure mode of an agent saying "all tests pass" when seven crates didn't compile is real, and it has to be addressed in the system prompt, not at code review time.

9. Make build and test one command. bun bd test <file> builds and runs. Agents don't have to remember a two-step incantation, which means they don't fail by forgetting step two. "Get your tests to pass. If you didn't run the tests, your code does not work" only works as an instruction if running tests is a single tool call.

10. Treat the meta-infrastructure as part of the work. The leaked .apply-lock-guard.sh is four lines. The leaked workflows are 4,000. The CLAUDE.md files in the repo are another 30 KB. None of this ships to users. All of it shipped Bun.

The footnote you actually want🔗

Bun's Rust binary is 3–8 MB smaller, the tests still pass, the benchmarks are neutral-to-faster, and the codebase is now in a language where the compiler can catch the bug class that has historically cost the team the most time. The proof of the approach is not the workflows — it's that one of the most hostile software-engineering tasks imaginable (rewriting a million-line systems codebase in a language with a completely different memory model, without breaking compatibility) shipped, with all tests green, in a single PR.

If you're considering whether agents can do real work: they can. The question is whether you're willing to build the harness.