{"id":22515,"date":"2026-07-21T09:41:01","date_gmt":"2026-07-21T09:41:01","guid":{"rendered":"https:\/\/scannn.com\/how-anthropic-runs-large-scale-code-migrations-with-claude-code-2\/"},"modified":"2026-07-21T09:41:01","modified_gmt":"2026-07-21T09:41:01","slug":"how-anthropic-runs-large-scale-code-migrations-with-claude-code-2","status":"publish","type":"post","link":"https:\/\/scannn.com\/lv\/how-anthropic-runs-large-scale-code-migrations-with-claude-code-2\/","title":{"rendered":"How Anthropic runs large-scale code migrations with Claude Code"},"content":{"rendered":"\n<div>\n<h2><strong>Six steps for large code migrations<\/strong><\/h2>\n<p><em>The process below has been generalized to be relevant to multiple languages and scenarios. For additional details, you can read <\/em><a href=\"https:\/\/bun.com\/blog\/bun-in-rust\"><em>Jarred\u2019s blog<\/em><\/a><em>.<\/em><\/p>\n<h3><strong>Prerequisites<\/strong><\/h3>\n<p>A prerequisite before starting on your migration project is to have a strong judge in place, otherwise you won\u2019t have an exit condition or measure of success.\u00a0<\/p>\n<p>The judge must be able to evaluate both the original code and the target code on equal terms. Test suites written in the original language will often depend on internal functions that won&#8217;t exist in the target code.\u00a0<\/p>\n<p>To build this judge:<\/p>\n<ul role=\"list\">\n<li><strong>Categorize existing tests<\/strong>. Use Claude to identify which tests are expressible as external calls and which depend on internals that won&#8217;t port.\u00a0<\/li>\n<li><strong>Rewrite for portability.<\/strong> Convert the external-facing tests into assertions that can run against both the original and the port. Use adversarial agents to verify the rewritten tests don&#8217;t weaken the assertions.<\/li>\n<li><strong>Validate the judge<\/strong>. Run it against the original code to confirm it passes. Then run it against deliberately broken code to confirm it fails \u2014 a judge that doesn&#8217;t catch breakage isn&#8217;t a judge.<\/li>\n<\/ul>\n<p>Jarred had a large test suite written in a third language (TypeScript), but that will not be the case for most projects. For his Python-to-TypeScript port, Mike created a parity harness of seven real-world scenarios and considered any behavior change a bug to be fixed.\u00a0<\/p>\n<p>Before we get into each stage, this graphic may help you follow along. This mostly follows Jarred\u2019s methodology, with reviews and gates at each stage. Mike followed a similar overall structure using similar loop workflows, but he ran the entire migration end to end, revised the rules and the workflow based on the results, and ran it again \u2014 discarding the output each time until the third run.<\/p>\n<figure style=\"max-width:1109pxpx\" class=\"w-richtext-align-fullwidth w-richtext-figure-type-image\">\n<div><\/div>\n<\/figure>\n<h3><strong>Step 1 \u2014 Create the rulebook, dependency map, and gap inventory<\/strong><\/h3>\n<figure style=\"max-width:733pxpx\" class=\"w-richtext-align-fullwidth w-richtext-figure-type-image\">\n<div><img decoding=\"async\" alt=\"\" src=\"https:\/\/cdn.prod.website-files.com\/68a44d4040f98a4adf2207b6\/6a58fa78da7794274460319e_Code-migration-step1.jpeg\" loading=\"lazy\"\/><\/div>\n<\/figure>\n<p>In this stage we are creating the foundations of our migration: an inventory of places where code will need to be refactored rather than just translated, a rulebook for how to translate our code, and a dependency map to order our migration implementation workstreams.\u00a0<\/p>\n<p>The order matters: the rulebook must come before the gap inventory. The gap inventory is defined by what the rulebook&#8217;s defaults won&#8217;t cover, and the two are tested together in a joint audit.<\/p>\n<h4><strong>Rulebook<\/strong><\/h4>\n<p>The exact shape of the <a href=\"https:\/\/github.com\/anthropics\/code-migration-kit-with-claude-code\/blob\/main\/templates\/RULEBOOK.md\">rulebook<\/a> depends on key architectural decisions you must make at the start. Chief among them, if the new code will follow the same structure, or if it will be completely redesigned.\u00a0<\/p>\n<p>If it\u2019s the former (Jarred), the rulebook will primarily be lookup tables that translates types and idioms between languages while pointing to the gap inventory for the harder-to-translate components. If it\u2019s the latter (Mike), it will be a design document.\u00a0<\/p>\n<p>Jarred created his rulebook by chatting with Claude, forming a policy for each area of ambiguity. He also used eight subagents specifically designed to review for 8 different categories of common failure modes based on his own intuition.<\/p>\n<h4><strong>Dependency map<\/strong><\/h4>\n<p>You need to understand file dependencies to effectively break up workstreams for a parallel migration so you know which files to migrate first and which files to contain in the same batch. Some languages and codebases have explicit manifests that make this easy, but for legacy codebases and many popular languages like C\/C++ and Python, these dependencies need to be discovered and mapped.<\/p>\n<p>Claude Code can deploy agents to create and run a deterministic script to produce this map. The <a href=\"https:\/\/github.com\/anthropics\/code-migration-kit-with-claude-code\/blob\/main\/prompts\/01-dependency-map.md\">prompt in the migration kit<\/a> uses a workflow to create a review-and-fix loop.\u00a0<em>Note: The starter kit is a generalized template of the process laid out in this post \u2014 it&#8217;s not what these specific ports ran on.<\/em><\/p>\n<h4><strong>Gap inventory and skeptic reviewers<\/strong><\/h4>\n<p>The new language has different requirements from the old language that must be met. For Zig to Rust the difference was manual memory management (C and C++ work the same way). For example:<\/p>\n<p>Zig<\/p>\n<pre contenteditable=\"false\" class=\"w-code-block\" style=\"display:block;overflow-x:auto;background:#2b2b2b;color:#f8f8f2;padding:0.5em\"><code class=\"language-plaintext\" style=\"white-space:pre-wrap\"><span><span>fn readConfig(allocator: std.mem.Allocator) ![]u8 {\n<\/span><\/span><span>    const buf = try allocator.alloc(u8, 1024);\n<\/span><span>    \/\/ ...fill buf...\n<\/span><span>    return buf; \/\/ caller must free this \u2014 but only the comment says so\n<\/span><span>}\n<\/span><span>\n<\/span><span>\/\/ A caller that forgets 'defer allocator.free(buf)' still compiles \u2014 the leak only surfaces at runtime.<\/span><\/code><\/pre>\n<p>Rust<\/p>\n<pre contenteditable=\"false\" class=\"w-code-block\" style=\"display:block;overflow-x:auto;background:#2b2b2b;color:#f8f8f2;padding:0.5em\"><code class=\"language-rust\" style=\"white-space:pre-wrap\"><span><span class=\"hljs-function\" style=\"color:#dcc6e0\">fn<\/span><span class=\"hljs-function\"> <\/span><span class=\"hljs-function\" style=\"color:#00e0e0\">read_config<\/span><span>() -&gt; <\/span><span style=\"color:#f5ab35\">Vec<\/span><span>&lt;<\/span><span style=\"color:#f5ab35\">u8<\/span><span>&gt; { \n<\/span><\/span><span><span\/><span style=\"color:#dcc6e0\">let<\/span><span> buf = <\/span><span style=\"color:#f5ab35\">vec!<\/span><span>[<\/span><span style=\"color:#f5ab35\">0u8<\/span><span>; <\/span><span style=\"color:#f5ab35\">1024<\/span><span>]; \n<\/span><\/span><span><span\/><span style=\"color:#d4d0ab\">\/\/ ...fill buf... <\/span><span>\n<\/span><\/span><span><span>buf <\/span><span style=\"color:#d4d0ab\">\/\/ ownership moves to the caller; memory is freed automatically <\/span><span>\n<\/span><\/span><span>} \n<\/span><span><span\/><span style=\"color:#d4d0ab\">\/\/ Use it after it's moved? Free it twice? Neither compiles. <\/span><span>\n<\/span><\/span><span><span\/><span style=\"color:#d4d0ab\">\/\/ Forget to free it? There's no free call to forget \u2014 drop is automatic.<\/span><\/span><\/code><\/pre>\n<p>For Python to TypeScript the gap was interfaces and contracts. Python doesn\u2019t require a contract declaring what shape of object it will accept or what it returns, but TypeScript does. For example:<\/p>\n<p>Python<\/p>\n<pre contenteditable=\"false\" class=\"w-code-block\" style=\"display:block;overflow-x:auto;background:#2b2b2b;color:#f8f8f2;padding:0.5em\"><code class=\"language-python\" style=\"white-space:pre-wrap\"><span><span class=\"hljs-function\" style=\"color:#dcc6e0\">def<\/span><span class=\"hljs-function\"> <\/span><span class=\"hljs-function\" style=\"color:#00e0e0\">register<\/span><span class=\"hljs-function\">(<\/span><span class=\"hljs-function\" style=\"color:#f5ab35\">handler<\/span><span class=\"hljs-function\">):<\/span><span>\n<\/span><\/span><span>    handler.setup()\n<\/span><span><span>    <\/span><span style=\"color:#dcc6e0\">return<\/span><span> handler.run({<\/span><span style=\"color:#abe338\">\"retries\"<\/span><span>: <\/span><span style=\"color:#f5ab35\">3<\/span><span>})\n<\/span><\/span><span>\n<\/span><span><span\/><span style=\"color:#d4d0ab\"># Any object with .setup() and .run() works here. Which objects actually get passed in? Read the whole codebase to find out.<\/span><span>\n<\/span><\/span><span\/><\/code><\/pre>\n<p>TypeScript<\/p>\n<pre contenteditable=\"false\" class=\"w-code-block\" style=\"display:block;overflow-x:auto;background:#2b2b2b;color:#f8f8f2;padding:0.5em\"><code class=\"language-typescript\" style=\"white-space:pre-wrap\"><span><span style=\"color:#dcc6e0\">interface<\/span><span> RunResult { ok: <\/span><span style=\"color:#f5ab35\">boolean<\/span><span> } \n<\/span><\/span><span>\n<\/span><span><span\/><span style=\"color:#dcc6e0\">interface<\/span><span> Handler \n<\/span><\/span><span><span>{ setup(): <\/span><span style=\"color:#f5ab35\">void<\/span><span>; \n<\/span><\/span><span><span>run(opts: { <\/span><span class=\"hljs-attr\">retries<\/span><span>: <\/span><span style=\"color:#f5ab35\">number<\/span><span> }): <\/span><span style=\"color:#f5ab35\">Promise<\/span><span>&lt;RunResult&gt;; \n<\/span><\/span><span>} \n<\/span><span>\n<\/span><span><span\/><span class=\"hljs-function\" style=\"color:#dcc6e0\">function<\/span><span class=\"hljs-function\"> <\/span><span class=\"hljs-function\" style=\"color:#00e0e0\">register<\/span><span class=\"hljs-function\">(<\/span><span class=\"hljs-function\" style=\"color:#f5ab35\">handler: Handler<\/span><span class=\"hljs-function\">): <\/span><span class=\"hljs-function\" style=\"color:#00e0e0\">Promise<\/span><span class=\"hljs-function\">&lt;<\/span><span class=\"hljs-function\" style=\"color:#00e0e0\">RunResult<\/span><span class=\"hljs-function\">&gt; <\/span><span>{ \n<\/span><\/span><span>handler.setup(); \n<\/span><span><span\/><span style=\"color:#dcc6e0\">return<\/span><span> handler.run({ <\/span><span class=\"hljs-attr\">retries<\/span><span>: <\/span><span style=\"color:#f5ab35\">3<\/span><span> }); } \n<\/span><\/span><span>\n<\/span><span><span\/><span style=\"color:#d4d0ab\">\/\/ The contract must be written down before this compiles<\/span><\/span><\/code><\/pre>\n<p>Both Jarred and Mike created gap inventory files capturing this implicit knowledge. Jarred inventoried these gaps up front, which is what we do here, while Mike chose to translate first and then create the gap inventory by auditing afterwards. You may need to do both.\u00a0<\/p>\n<p>Check out this sample <a href=\"https:\/\/github.com\/anthropics\/code-migration-kit-with-claude-code\/blob\/main\/prompts\/02-gap-inventory.md\">Claude Code prompt to create a gap inventory file<\/a>.<\/p>\n<h3><strong>Step 2 \u2014 Stress-test the rules\u00a0<\/strong><\/h3>\n<figure style=\"max-width:744pxpx\" class=\"w-richtext-align-fullwidth w-richtext-figure-type-image\">\n<div><img decoding=\"async\" alt=\"\" src=\"https:\/\/cdn.prod.website-files.com\/68a44d4040f98a4adf2207b6\/6a58fbe31db98b8b59d39ba8_MIgration-step2.jpeg\" loading=\"lazy\"\/><\/div>\n<\/figure>\n<p>This step involves a mini-migration that serves as a \u201cshakedown cruise\u201d for the larger migration.\u00a0<\/p>\n<p>In this step, Jarred used one agent to translate three files using the rulebook, one agent to translate three files \u201clike a senior Rust engineer,\u201d and one agent to use the diff to create new translation rules. At this stage he caught two critical issues that would have created numerous issues if fanned out across all 1,448 files.<\/p>\n<p>The prompt may look something like <a href=\"https:\/\/github.com\/anthropics\/code-migration-kit-with-claude-code\/blob\/main\/prompts\/03-stress-test.md\">this<\/a>.<\/p>\n<p>This type of stress test <strong>only works for structure-preserving migrations<\/strong>, where two translations of the same file are comparable line by line.\u00a0 If your rulebook is a redesign \u2014 like Mike&#8217;s \u2014 the equivalent test is attacking the design document directly with adversarial reviewers, then validating it with a disposable end-to-end run.<\/p>\n<p>Regardless, throw out any translated files. The goal is to refine the rules, not make incremental progress.<\/p>\n<h3><strong>Step 3 \u2014 Translate everything\u00a0<\/strong><\/h3>\n<figure style=\"max-width:751pxpx\" class=\"w-richtext-align-fullwidth w-richtext-figure-type-image\">\n<div><img decoding=\"async\" alt=\"\" src=\"https:\/\/cdn.prod.website-files.com\/68a44d4040f98a4adf2207b6\/6a58fd9c8899de91189a89e0_ai-migation-step4-5-6.jpeg\" loading=\"lazy\"\/><\/div>\n<\/figure>\n<p>For the remaining steps, you run the same multi-agent loop architecture: implement, review, and fix.\u00a0<\/p>\n<p>You can offload implementer work to smaller models and keep reviewers on larger ones. For example, Mike used Claude Sonnet when he fanned out 12 subagents for the main migration.\u00a0<\/p>\n<p>The work queue should be mechanical. A batch script decides what\u2019s done by checking whether the translated file exists on disk, then slices the pending files into batches for the implementer agents. Because the queue is rebuilt from disk every time, the migration is resumable by construction.<\/p>\n<p>At this stage, agents can be overly cautious with how much work they do. The fix can be a blunt, emphatic prompt instruction with context that the compiler will catch mistakes in the next step. <\/p>\n<p>Anything the translator can\u2019t execute confidently gets flagged with \/\/ TODO(port): &lt;reason&gt; to be dealt with in step 4. From here on, the to-do lists write themselves: the compiler enumerates the errors, the smoke tests find the crashes, the suite reports the failures.<\/p>\n<p>Two adversarial reviewers evaluate the work of the implementers using separate contexts and disagreement between reviewers goes to a third agent. When a reviewer keeps catching the same mistake across files, the fix isn&#8217;t per-file. You add one sentence to the rulebook and regenerate the affected batch. The rulebook keeps growing through this step; the code never gets hand-patched against it.<\/p>\n<p>One important design decision to note in this step is where the compiler sits. Mike ran the TypeScript compiler inside every loop, because it checks a unit in seconds. Jarred banned the compiler from the loop entirely and deferred it to the next step, because cargo takes minutes.<\/p>\n<p>At this step, much of the heavy lifting has been done and <a href=\"https:\/\/github.com\/anthropics\/code-migration-kit-with-claude-code\/blob\/main\/prompts\/04-translation-kickoff.md\">the prompts start to get shorter.<\/a><\/p>\n<h3><strong>Steps 4, 5, 6 \u2014 Compile, run, and match behavior<\/strong><\/h3>\n<figure style=\"max-width:746pxpx\" class=\"w-richtext-align-fullwidth w-richtext-figure-type-image\">\n<div><img decoding=\"async\" alt=\"\" src=\"https:\/\/cdn.prod.website-files.com\/68a44d4040f98a4adf2207b6\/6a58fd76153c143199b57e1b_ai-migration-step3.jpeg\" loading=\"lazy\"\/><\/div>\n<\/figure>\n<p>These three steps share the same loop architecture and need progressively less human judgment, so we cover them together.<\/p>\n<p><strong>Step 4<\/strong>, for example, may often dissolve into step 3 depending on the language and size of the migration.\u00a0<\/p>\n<p>Depending on the size and difficulty of the compiler step, agents may not run this at all. Jarred executed this with an orchestrator script that invoked the compiler once across the whole workspace. \u201cFixer agents\u201d then ran through the error list in parallel with adversarial review. The build runs again, rinse and repeat.\u00a0<\/p>\n<p>Reviewing the error list is helpful to catch systemic issues that may require adjustments. For example, Jarred ran into thousands of Rust module errors that surfaced after fixing cyclic imports that Zig&#8217;s lazy compilation tolerated. He fixed the loop by encoding logic to classify which dependence to delete, move, or restructure the boundary.\u00a0<\/p>\n<p><strong>Step 5 <\/strong>also has a mechanical source of truth similar to the compiler error list: crashes from the smoke test. Again, the loop fix was to group issues into categories, in this case grouping causes by root cause that are reviewed by adversarial subagents.\u00a0<\/p>\n<p><strong>Step 6<\/strong> and the end of our story is comparing the programs\u2019 behavior across the two codebases.<\/p>\n<p>Our files have now been translated, compiled, and smoke tested. Now it&#8217;s time to shard them and run the test suite (from the prerequisite stage) against them. Tackle failures with &#8220;fixer agents&#8221; that review the failed tests against both codebases. Adversarial reviewers check their fixes.<\/p>\n<p>The next stage in this loop is a <a href=\"https:\/\/github.com\/anthropics\/code-migration-kit-with-claude-code\/blob\/main\/scripts\/build_daemon.sh\">build daemon<\/a>, which is the only process allowed to rebuild the binary. Fixers write patches; the daemon batches them, rebuilds once, re-runs the affected tests, and feeds the results back. This serializes the most expensive operation instead of letting multiple agents trigger it independently.<\/p>\n<p>When the same failure repeats across many tests, the fix moves upstream: you amend the rule that produced the bug and regenerate only the files that rule touched.<\/p>\n<p>Mike&#8217;s approach matters here, because many developers won&#8217;t have a built-out or ported test suite. Mike had Claude create a small script to run 7 real-world scenarios against both the new port and the original Python codebase, and diffed the results. Each failing scenario got its own fix agent, and the loop ran until all seven passed.<\/p>\n<p>Then he went one step further. Claude designed its own end-to-end test suite and ran it autonomously overnight, fixing what broke and re-running four nights in a row. As a result, it caught the paper cuts no scenario list would have predicted.<\/p>\n<p>The lesson is that a missing test suite doesn&#8217;t block this step. If you can&#8217;t inherit a referee, have Claude build one. Your original codebase is the ground truth either way.<\/p>\n<h2><strong>Code migrations best practices<\/strong><\/h2>\n<p>Every run taught us something the previous one didn&#8217;t. It\u2019s a safe bet your next migration will teach you things this guide can&#8217;t. But a few practices held up across every project:<\/p>\n<ul role=\"list\">\n<li><strong>Don&#8217;t follow this guide blindly.<\/strong> Each migration is different. Treat this as a starting point, and plan your specific migration with Claude before committing to it.<\/li>\n<li><strong>Don\u2019t focus on individual failures.<\/strong> Individual failures are the loop&#8217;s job. Fixer agents burn those down. Your attention belongs on the patterns.\u00a0<\/li>\n<li><strong>Make review adversarial and verification mechanical.<\/strong> Adversarial review allows for longer running tasks and is often worth the token consumption. Let scripts \u2014 a compiler, a diff, a test suite \u2014 be the referee.\u00a0<\/li>\n<li><strong>Don&#8217;t use the largest model for everything.<\/strong> Token spend concentrates in your loops, so design them deliberately. Smaller models handle the high-volume implementation fan-out well; save your largest model for reviewers and for anything that writes rules other agents will follow.<\/li>\n<li><strong>Front-load the human hours.<\/strong> The rulebook and the stress test are the most time-consuming. Everything after is mostly queues burning down.\u00a0<\/li>\n<li><strong>Make the work queue mechanical and resumable.<\/strong> Done should mean &#8220;the output file exists on disk.&#8221;\u00a0<\/li>\n<\/ul>\n<h2><strong>Review loop results, not code<\/strong><\/h2>\n<p>Jarred\u2019s Bun migration is now in production, although every migration has tradeoffs. For example, about 4% of the Rust code sits inside &#8220;unsafe&#8221; blocks, mostly single-line pointer operations at C\/C++ boundaries.<\/p>\n<p>But the new codebase is measurably better. Every memory leak the team&#8217;s tooling can detect has been fixed: one benchmark of 2,000 repeated builds dropped from 6,745 MB of memory to 609. The binary is 19% smaller on Linux and Windows. And cross-language optimization made it 2\u20135% faster across HTTP serving and real-world workloads like next build and tsc.\u00a0<\/p>\n<p>Consider whether it\u2019s time to re-run the math of your long deferred migration. Pick the codebase you&#8217;ve been tolerating and ask Claude what the migration process looks like for it.<\/p>\n<p><strong><em>Related<\/em><\/strong><\/p>\n<p>&#x200d;<\/p>\n<\/div>\n<p><a href=\"https:\/\/claude.com\/blog\/ai-code-migration?utm_source=tldrnewsletter\">Source link <\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Six steps for large code migrations The process below has been generalized to be relevant to multiple languages and scenarios. For additional details, you can read Jarred\u2019s blog. Prerequisites A prerequisite before starting on your migration project is to have a strong judge in place, otherwise you won\u2019t have an exit condition or measure of [&hellip;]<\/p>\n","protected":false},"author":16,"featured_media":22470,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[143],"tags":[],"class_list":["post-22515","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai"],"_links":{"self":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/22515","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/users\/16"}],"replies":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/comments?post=22515"}],"version-history":[{"count":0,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/22515\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media\/22470"}],"wp:attachment":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media?parent=22515"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/categories?post=22515"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/tags?post=22515"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}