Anthropic's Six-Step Code Migration Playbook: Moving Bun's Million Lines from Zig to Rust in Two Weeks

The templates and prompts are open-sourced; the Bun migration burned 5.9 billion uncached input tokens, roughly $165,000 at API prices.
By Anthropic / ClaudeDevs07-22 · 12 min read⚑ Tutorial / how-to
🎧 Listen to the podcast0:00 / --:--
0:00 / --:--
Tool Guide · Analysis by Xiaohu
Quick Take
  • Rewriting a production codebase in a new language typically costs about four years, $3–4 million, and often stalls halfway. Anthropic's team migrated 10 code packages in the past month; the Bun migration alone moved a million lines in under two weeks, with the original test suite fully green before the merge.
  • The core principle is simple: don't fix the code, fix the loop that produces it. When the same mistake appears across dozens of files, the right move is to add a rule to the rulebook and rerun the whole batch — not to patch files one by one.
  • Before you start, you need a judge. Turn the existing tests that can run on both the old and new code into assertions, then validate the judge by running it against deliberately broken code. A judge that can't catch bad code isn't a judge — without it, you have no way to define "done."
  • The engineering details are copy-paste ready: the work queue rebuilds from disk each time so it can resume after interruptions; implementation uses a smaller model, review uses a larger one; two independent review contexts that escalate to a third Agent when they clash; and anything you can't port gets a // TODO(port) marker for later.
  • There's a clear ledger of costs and benefits: the Bun project consumed 5.9 billion uncached input tokens and 690 million output tokens, roughly $165,000 at API prices. Mike Krieger's project saw compilation drop from 8 minutes to about 2 seconds after the move, startup got 6x faster, and an entire deployment pipeline was retired.
⚑This is Anthropic's own material about using its models. Both case studies feature their own people, and all numbers come from their own reporting. Read the data with that in mind.
1The Opening

Ten code packages in one month

A million lines of code, a new language, two weeks to migrate, and the full existing test suite passed in CI before the merge.

This is a practical walkthrough from Anthropic's official ClaudeDevs account, detailing how they've spent the past month using Claude Code for code migration — moving a production codebase to an entirely new language. The methodology, along with its templates, is laid out openly for anyone to copy, running on Claude Fable 5, Claude Opus 4.8, and Claude Code's dynamic workflows.

Over the past month, Anthropic's developers have migrated 10 code packages, ranging from tens of thousands to hundreds of thousands of lines. Only two are detailed in the post.

Bun · Zig → Rust
1M linesof code produced in under two weeks
100%Bun's test suite passing in CI before the merge
19regressionsFeatures that worked before, now broken after the change. Not the same as "new feature bugs." found post-merge, all fixed
JuneRust version running inside Claude Code
Mike Krieger · Python → TypeScript
1 weekendto produce 165K lines of TypeScript
Hundredsof agents used during the migration
8stage gatesBreak the process into segments with a check at the end of each; you can't proceed until you pass., plus 3 rounds of adversarial review
Command-by-commandfinal reconciliation by diffing the output of every command between old and new

Jarred Sumner—Bun's co-founder, now on Anthropic's technical staff—led the Bun migration. Mike Krieger, Instagram's co-founder and now co-head of Anthropic Labs, handled the Python side.

GitHub page for Jarred Sumner's million-line PR
GitHub page for Jarred's million-line PR. Source: ClaudeDevs original post
2The Changing Calculus

Why nobody dared before

Jarred chose Zig back in the day because it could match C-level performance while staying simple enough — in his words — for one person in a small Oakland apartment, without the help of large models, to write Bun in a year. That simplicity came at a cost that's been compounding. Bun's CLI now gets over ten million downloads a month, and Claude Code itself relies on it heavily.

Until last quarter, those costs didn't justify freezing the roadmap and betting resources on a multi-quarter project.

Before last quarter
  • 4-year timeline, $3–4M in engineering resources
  • Feature freeze during migration
  • Maintaining two codebases in parallel for quarters or even years
  • Worst case: you get 90% of the way there and end up in a messier state than when you started
Now
  • Tens to hundreds of thousands of dollars — still real money
  • Worst case: delete the branch and try again
  • You no longer need "we can't survive without this" to justify starting
  • A memory bug patch sitting in the changelog for a year, or a long-standing bottleneck, is enough

Mike's project was driven by exactly that kind of bottleneck. His team's internal tool had to be packaged as a single executable for users, and the Python toolchain took about eight minutes to compile for each platform. Running the whole build matrix meant waiting thirty minutes for every release. After the move to TypeScript, the same compilation took about two seconds, the executable started 6x faster, and the team retired an entire separate deployment pipeline.

3The Core Principle

Same error repeated? Fix the rule

The most valuable line in the entire post is this:

The core insight is that you don't fix the code. You fix the process (loop) that produced the code.The key insight: don't fix the code; fix the loop that produces it. · ClaudeDevs

In practice: when a review agent spots the same mistake across dozens of files, don't reach for those files. You add a line to the rulebook and regenerate the affected batch. The rulebook thickens as the process continues; code is never hand-patched to comply.

Rulebook Every review comment must cite a rule from it Regenerate entire batch Implementation Agent Small model Adversarial Review A Adversarial Review B Each with independent context Third Agent Adjudicates When the two disagree Affected batch · discarded Same error seen across files → add a line to the rulebook
Illustration based on the original text. The bold line is the key takeaway: errors don't fall back into individual files, they fall back into the rulebook.
Not just rhetoric

Jarred set up reviews and gates at every stage. Mike took it further: he ran the entire migration end-to-end, revised the rules and workflows based on the results, then ran it again. He discarded each pass's output, kept only the improvements, and the third pass is what stuck.

Two complete migrations ended up in the trash. What survived was an improved rulebook. "The output is disposable; the process is the asset" was followed literally here.

Why code migration fits this pattern so well

  • It's embarrassingly parallel. The work can be split into thousands of independent units — files, crates — that agents can work on simultaneously without waiting for each other.
  • Context is clear and complete. The old code itself is an excellent spec for the model.
  • The judge already exists. Most large codebases ship with a test suite agents can use to verify their work.
  • The queue grows itself. Compiler errors and test failures become the next task for the next agent.
  • It demands consistency and edge-case handling. Reviewers must cite a specific rule for every comment, turning a one-off violation into a queue item rather than letting it slip by.
4The Prerequisite

Build a judge before you start

Before translating any code, build the judge. That's the one prerequisite. Without it, there's no exit condition, no measure of success, no way to know when you're done or if it's correct.

1. Classify existing testsWhich can be verified externally
Which depend on internals and can't be ported
→
2. Rewrite as portable assertionsThe same assertions
must run on both old and new code
→
3. Validate the judge itselfMust pass on original code
Must fail on deliberately broken code
Three steps to build a judge
1. Classify existing tests: use Claude to identify which tests can be verified via external calls and which depend on internal implementation details that can't be ported.
2. Make them portable: rewrite the external-facing tests into assertions that can run against both the original and the migrated code. Have an adversarial agent check whether the rewritten assertions have been weakened.
3. Validate the judge: run it against the original code to confirm it passes, then run it against deliberately broken code to confirm it fails. A judge that can't catch breakage isn't a judge.

The "adversarial agent checks for weakened assertions" in step two is not optional. When rewriting tests, loosening an assertion instantly boosts the pass rate — but you've just lost your only correctness check. Step three is the same idea: a test suite that passes everything looks like a judge, but it's really just a rubber stamp.

5Step One

Rulebook, dependency graph, gap inventory

Order matters here: the rulebook must come before the gap inventory. That's because the gap inventory is defined as "what the rulebook's default rules don't cover." You need the former for the latter to even make sense. Both go into the same joint review at the end.

What the rulebook looks like depends on a decision you make at the start

Is the new code going to mirror the original structure, or are you rearchitecting it completely? That call shapes the entire rulebook.

Preserve structure · Jarred's path

The rulebook is mostly a set of lookup tables: how this type is written on the other side, what this idiom maps to here. Anything tricky points to the gap inventory.

Jarred worked it out with Claude in conversation, codifying a policy whenever they hit an ambiguity.

He also set up 8 sub-agents based on the failure patterns he hit, each one checking for a common class of mistake.

Full redesign · Mike's path

The rulebook is a design document, not a translation lookup table.

On this path, "did we translate it right" takes a backseat. "Is the design right" becomes the primary risk, and the verification approach changes accordingly (more on that in the next section).

Dependency graph and gap inventory

The dependency graph splits the parallel workflow: you need to know which files come first and which must be processed in the same batch. Don't do it by hand — have Claude Code spawn an agent to write a deterministic graph-generating script.

The gap inventory holds what the new language requires that the old one didn't. For Zig → Rust, the gap is manual memory management (the C and C++ way). For Python → TypeScript, it's interfaces and contracts. The next section digs into this with actual code from the article.

6What a Gap Looks Like

What Zig let slide, Rust makes you confront

Here's the same function—allocate a buffer, fill it, return it to the caller. The difference isn't syntax—it's who's responsible for cleanup.

Zig · Who frees this? Only the comment knows.
// Zig

fn readConfig(allocator: std.mem.Allocator) ![]u8 {
const buf = try allocator.alloc(u8, 1024);
// ...fill buf...
return buf; // caller must free this — but only the comment says so
}

// A caller that forgets 'defer allocator.free(buf)' still compiles — the leak only surfaces at runtime.
The caller must free the returned buffer, but the code relies on a comment to say so. A caller that forgets to free compiles fine; the leak only shows up at runtime.
Rust · Ownership moves with the return value
fn read_config() -> Vec<u8> {
    let buf = vec![0u8; 1024];
    // ...fill buf...
    buf // ownership moves to the caller; memory is freed automatically
}

// Use it after it's moved? Free it twice? Neither compiles.
// Forget to free it? There's no free call to forget — drop is automatic.
Ownership transfers to the caller with the return value, and memory is cleaned up automatically. Use something after it's been moved? Free it twice? Neither compiles. Forget to free it? There's no free call to forget.
What "ownership" means

Borrowing a book ends in one of two ways: you return it, or you pass it on to someone else. The book is always in exactly one person's hands, and the library itself knows when to reclaim it — nobody has to remember to return it. With Zig, you get a sticky note saying "remember to return this," and nothing stops you if you don't.

Python → TypeScript gets stuck on interface contracts. Python doesn't require you to declare what shape of object a function accepts or returns; TypeScript does. That info used to live only in the code author's head, so it has to be dug out and written down. That's the gap inventory's job.

Jarred and Mike both created gap inventory files, but at different times: Jarred mapped gaps upfront (as step one prescribes), while Mike translated first, then audited to fill gaps. You might need to do both.

7Step Two

Stress-test the rules on three files

Once the rulebook is written, don't roll it out everywhere. Jarred started step two with three agents and only three files to work on.

Agent 1

Translate the 3 files, strictly following the rulebook.

Agent 2

Translate the same 3 files, "like a senior Rust engineer would."

Agent 3

Read the diff between the two translations and write new translation rules based on it.

On just those three files, he caught two critical issues. This migration involved 1,448 files in total. Had he rolled out immediately, those two problems would've infected every file and needed hunting down one by one.

Flowchart for step two: stress-testing the rules
Flowchart for step two. Source: ClaudeDevs original post

When this step is done, all the translated files are thrown away. The goal is to refine the rules, not to bank progress.

This only works for structure-preserving migrations—the two translations of the same file line up, and their differences are meaningful. If your rulebook is a redesign document (like Mike's), the equivalent move is to have the adversarial review attack the design document directly, then validate it with a throwaway end-to-end run.
8Steps Three Through Six

The queue rebuilds from disk, so it can always resume

With a validated rulebook and a working judge, you're finally ready for the full translation. From here on, every remaining step runs the same multi-agent loop: implement, review, fix.

Diagram of the multi-agent loop for the full translation step
The multi-agent loop for "step three: full translation." Source: ClaudeDevs original post

Which model runs what

Implementation work can be delegated to a smaller model, while review stays on the larger one. Mike's main migration phase spun up 12 Claude Sonnet sub-agents to handle the implementation.

Third Agent Adjudicates When the two reviewers clash Adversarial Review A Large model · Independent context Adversarial Review B Large model · Independent context Implementation layer · 12 sub-agents running on Claude Sonnet
Illustration based on the original text. Numbers reflect Mike's main migration phase setup.

The queue has to be mechanical

The batch script marks a file done based on one thing only: whether the translated file exists on disk.k. Then it chunks the remaining work into batches and hands them to the implementation agents.

Scan the disk
→
Translated file exists = done
→
Chunk the remaining work
→
Send to implementation agentsThe queue rebuilds from scratch
each time, so it can always resume

When a translation agent hits something it's not sure about, it doesn't guess and doesn't force it. It just drops a marker and moves on, leaving it for step four to handle in one pass:

The standard hold marker
// TODO(port): <reason>

On the review side, two adversarial reviewers evaluate the implementations, each with independent context. When they disagree, a third agent adjudicates. When a reviewer keeps catching the same mistake across multiple files, the fix is a new line in the rulebook, followed by regenerating that batch.

There's a point worth remembering here: where you put the compiler. Mike put the TypeScript compiler inside every loop, because checking a single unit only takes seconds. Jarred pushed the compiler entirely out of the loop and into the next step, because a single cargo build takes minutes. The deciding factor is how expensive a single compile is, not which approach is more "proper."

Steps 4, 5, 6 · Compile, get it running, align the behavior

These three steps share the same loop architecture, but each one requires less human judgment than the last.

Flow diagram for steps four, five, and six: compile, run, align behavior
The loop structure for steps 4, 5, and 6. Source: ClaudeDevs original post

Jarred used an orchestration script that invokes the compiler once on the entire workspace and collects a full list of errors. "Fix agents" then work through that list in parallel, with adversarial review, then it rebuilds and does another pass.

Reading the error list itself is useful — it surfaces systemic problems that need process changes. Jarred hit one: after fixing circular imports, thousands of Rust module errors appeared. Zig's lazy compilation tolerated those cycles; Rust doesn't. His fix was to encode logic into the loop that would classify each dependency issue — whether it should be deleted, moved, or whether the module boundary needed to be reworked.

Step five has its own mechanical source of truth: crashes from a smoke test. The loop-level fix is to group the problems by root cause and route them to adversarial sub-agents for review.

Step six is about getting the behavior of the two programs to match. The files are translated, compiled, and passing the smoke test. Now you split them into slices and run the test suite you built in the prerequisite phase. Failures go to a "fix agent" that looks at the failing test against both codebases, and the fix is then checked by an adversarial reviewer.

This step also introduces a new character: the build daemon. It's the only thing in the entire process that's allowed to rebuild the executable. Fix agents just write patches. The daemon collects a batch of patches, rebuilds once, re-runs the affected tests, and feeds the results back. The most expensive operation gets serialized instead of being triggered by a dozen different agents all at once.

What if you don't have a test suite?

Lots of people don't have a portable test suite sitting around. Mike didn't. He had Claude write a small script that ran 7 real-world scenarios against both the new port and the original Python version, then diffs the results. Each failing scenario got its own fix agent, and the loop ran until all 7 passed.

Then he went a step further: he had Claude design its own end-to-end test suite, run it autonomously for four consecutive nights, fix what broke, and re-run it. That caught the kind of subtle issues no scenario checklist could have anticipated.

The takeaway: a missing test suite is not a blocker. If you can't inherit a judge, have Claude build one. Either way, your existing codebase is the ground truth.

After the move

Jarred's Bun migration is now running in production, and the cost is visible on the other side of the ledger: roughly 4% of the Rust code lives inside unsafe blocks, mostly single-line pointer operations at the C/C++ boundary.

6,745 → 609 MB
Memory usage in a benchmark running 2,000 repeated builds; all memory leaks detectable by the team's tools have been fixed
19% smaller
Executable size on Linux and Windows
2–5% faster
Speedup from cross-language optimizations on real workloads like HTTP serving and next build, tsc
4%
Share of Rust code in unsafe blocks — the price paid for this migration

Practices that hold up on every project

  • Don't blindly follow this guide. Every migration is different. Treat it as a starting point, and plan your own with Claude before you begin.
  • Don't focus on individual failures. Route single failures back into the loop. Your attention belongs on the patterns.
  • Reviews should be adversarial, verification should be mechanical. Let the compiler, the diff, and the test suite be the judges.
  • Don't use the biggest model for everything. Spread high-volume implementation work across smaller models. Save the biggest model for review and for anything that has to write rules others will follow.
  • Front-load the human effort. The rulebook and the stress tests take the most time. After that, the queue basically burns itself down.
Overview diagram of the six-step process
Overview of the six-step process. Source: ClaudeDevs original post
9Where to Get the Materials & How to Read Them

Where to get the templates, and what to take with a grain of salt

You don't have to write any of this from scratch. Anthropic has open-sourced the templates and prompts.

Ready-made materials
Repo: https://github.com/anthropics/code-migration-kit-with-claude-code
Rulebook template: templates/RULEBOOK.md
Gap inventory prompt: prompts/02-gap-inventory.md
Build daemon script: scripts/build_daemon.sh
Jarred's detailed write-up: https://bun.com/blog/bun-in-rust

A few things need context. That $165,000 figure is calculated by taking 5.9 billion uncached input tokens and 690 million output tokens and pricing them at public API rates. "Uncached" means it doesn't benefit from the repeated-content discount. This isn't necessarily what Anthropic actually paid internally. The main phase of Mike's migration was 27 million tokens.

The post says 10 code packages were migrated in the past month, but only 2 are covered. The other 8 — what they were, how they went — get no mention at all. It also doesn't say how severe those 19 post-merge regressions in Bun were, or how many users they affected.

Both case studies feature Anthropic insiders: one is their own technical staff, the other is the co-lead of their own Labs division. The method itself is verifiable — you can run it yourself and see if it works. But whether the success rate is genuinely "10 out of 10" or just "the ones we chose to tell you about" is something this material can't confirm.

✅ Code Migration Checklist
The rulebook template, gap inventory prompt, and build daemon script are all in the anthropics/code-migration-kit-with-claude-code repo. Follow the order above and you can get started.
Source
How Anthropic runs large-scale code migrations with Claude CodeAnthropic / ClaudeDevs·x.com·2026-07-21
About this page
The five screenshots are from the original post. The loop and agent-tier diagrams are original illustrations based on the text. The $165,000 figure is an estimate at public API prices, not the internal cost. The article only details 2 of the 10 migration projects.
↑ End of article
✓ This article is free to read in full

Members can copy or download the full Markdown, and let their AI subscribe and ask.

Read the original ↗
📮 New explainers, straight to your inbox

Found this useful? Leave your email — every new explainer, the moment it's out. Unsubscribe anytime.