Anthropic's Six-Step Code Migration Playbook: Moving Bun's Million Lines from Zig to Rust in Two Weeks
- 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.
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.
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.
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.
- 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
- 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.
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.
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.
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.
Which depend on internals and can't be ported
must run on both old and new code
Must fail on deliberately broken code
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.
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.
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.
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.
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
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.
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.
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.
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.
Translate the 3 files, strictly following the rulebook.
Translate the same 3 files, "like a senior Rust engineer would."
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.
When this step is done, all the translated files are thrown away. The goal is to refine the rules, not to bank progress.
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.
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.
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.
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:
// 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.
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.
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.
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.
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.
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.
Rewriting a Million Lines in Two Weeks: Anthropic's Code Migration Playbook, Explained
Anthropic's official account published its full internal playbook for code migration, templates and prompts included. Here's the core method in one page, with pictures.
↓ One page · There's an animated diagram
Rewriting an entire production codebase in a different language is called a code migration. Anthropic's official ClaudeDevs account published exactly how they've been doing it this past month: 10 code packages migrated, ranging from tens of thousands to hundreds of thousands of lines. The post details only two of them.
All of these numbers come from the ClaudeDevs post. The two case studies feature Anthropic insiders: one is their own technical staff, the other is Mike Krieger, co-lead of Anthropic Labs and Instagram co-founder. No third party has independently replicated these results.
Before last quarter, the math on this never worked out.
- 4-year timeline, $3–4M in engineering resources
- Roadmap frozen, no new features for the duration
- Maintaining two codebases in parallel for quarters or even years
- Worst case: you get 90% of the way there and end up in a worse state than no migration at all
- 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 the effort
- A year-old memory bug patch in the changelog, or a long-standing bottleneck, is enough
Mike Krieger's project was forced into existence by a single bottleneck. His team's internal tool has to be packaged as a single executable for users. The Python toolchain takes eight minutes to compile for each platform.
Bar lengths proportional to time, with the ~2-second bar scaled up to be visible. That's the result of Mike Krieger's weekend: 165K lines of TypeScript, compile time from 8 minutes to 2 seconds, 6x faster startup, and an entire deployment pipeline retired. Numbers from the same article.
When something gets repeated across your codebase, don't fix the files. Fix the loop that produces them. Concretely: when a review agent keeps catching the same mistake across dozens of files, you add one line to the rulebook and regenerate the whole affected batch. The rulebook only gets thicker; code is never manually patched to fit it.
A judge is the standard that tells you whether the migration is correct. Without it, you have no exit condition and no way to know when you're done. The original article puts this before step one.
The templates and prompts are open-sourced in the anthropics/code-migration-kit-with-claude-code repo on GitHub, including the rulebook template and build scripts. The method is copy-paste ready. Here's the cheat sheet.
✔ Implementation on small models, review on large ones
✔ Work queue rebuilds from disk each time; file exists = done
✔ Can't translate it? Mark it // TODO(port): reason, handle it later
✔ Stress-test rules on 3 files before the full rollout; throw away the output, keep the rules
✘ Using the biggest model for everything
✘ Storing progress in an Agent's memory, losing it all on a crash
✘ Guessing when you're not sure
✘ Rolling out to all 1,448 files before you've validated the rules
- × $3–4 million
- × Roadmap frozen for 4 years
- × Maintaining two codebases
- × Getting stuck at 90%
when it's done?
2. Turn them into assertions
3. Test the judge on deliberately broken code
IN 60 FILES!
5.9B uncached input tokens
Members can copy or download the full Markdown, and let their AI subscribe and ask.
Found this useful? Leave your email — every new explainer, the moment it's out. Unsubscribe anytime.