| name | claude-code-migration-kit |
| description | Run large-scale language migrations with Claude Code using structured prompts, dependency mapping, and adversarial review |
| triggers | ["migrate codebase to another language","run language migration with claude","translate code from one language to another","port codebase to new language","setup code migration workflow","create migration rulebook","generate dependency map for migration","stress test migration rules"] |
Claude Code Migration Kit
Skill by ara.so — Claude Code Skills collection.
A structured framework for running large-scale, total language migrations using Claude Code. This kit provides prompts, templates, and scripts for structure-preserving migrations (same architecture, new language) where the entire codebase crosses over and the old language is deleted.
What It Does
- Provides 8 sequenced prompts (feasibility → post-parity) for methodical migration
- Generates dependency maps to order translation work
- Creates rulebooks that decide every translation question once
- Implements adversarial review (implementer + 2 reviewers + fixer per unit)
- Uses parallel agent workflows with explicit sign-off gates
- Validates through parity testing against original codebase
Core doctrine: You don't fix the code — you fix the process that produced the code.
Installation
git clone https://github.com/anthropics/code-migration-kit-with-claude-code ./migration-kit
cp -r migration-kit/skill ~/.claude/skills/code-migration
cp migration-kit/CLAUDE.md ./CLAUDE.md
mkdir -p migration
The Six-Step Process
Step 0: Feasibility Assessment
Always start here. Paste prompts/00-feasibility.md with placeholders filled:
Source language: [Python]
Target language: [Rust]
Repository path: [./src]
Approximate file count: [450]
Primary reason for migration: [memory safety + performance]
The feasibility prompt produces:
- Case for staying vs. migrating
- Structure-preserving vs. redesign recommendation
- Verification cost estimate
- Custom six-step sketch for your repo
- Go/no-go verdict
If redesigning instead of structure-preserving:
- Rulebook becomes a design document
- Bakeoff step is invalid (use adversarial design review + disposable full runs)
- Unit of work is module/subsystem, not file
- Behavior matching still works unchanged
Step 0b: Judge Setup (Critical)
You need a judge before Step 1. If your test suite imports internals that will die with the old language, run prompts/00b-judge-setup.md:
I need to build a portable parity harness because our tests import [language]-specific internals.
Source language: [Python]
Target language: [Rust]
Test suite path: [./tests]
Public API surface: [CLI + HTTP API]
The judge must be:
- Validated against the original code (zero failures)
- Validated against deliberately broken code (catches intentional bugs)
- Kept running throughout the migration
- Language-agnostic (no imports of source internals)
Step 1: Create Map and Rules
Three parallel artifacts:
1. Dependency Map
python migration-kit/scripts/depmap_python.py ./src > migration/depmap.json
node migration-kit/scripts/depmap_js.mjs ./src > migration/depmap.json
python migration-kit/scripts/depmap_c_headers.py ./include > migration/depmap.json
The map provides:
- File-level dependency ordering (leaves to root)
- Package-level cycle detection
- Translation queue foundation
2. Rulebook
cp migration-kit/templates/RULEBOOK.md ./migration/RULEBOOK.md
Use prompts/01-create-rulebook.md to draft it. The rulebook decides:
- How each source construct translates to target
- Naming conventions (snake_case → camelCase, etc.)
- Error handling patterns
- Memory management (if applicable)
- Testing approach per translated unit
Meta-rule: If two agents could answer differently, it goes in the rulebook.
Example rulebook entry:
## Error Handling
**Source (Python):**
```python
def parse_config(path):
try:
return json.load(open(path))
except FileNotFoundError:
return {}
Target (Rust):
fn parse_config(path: &Path) -> Result<Config, ConfigError> {
let contents = fs::read_to_string(path)
.map_err(|e| ConfigError::ReadFailed(path.to_owned(), e))?;
serde_json::from_str(&contents)
.map_err(ConfigError::ParseFailed)
}
Rule: All Python exceptions become Result<T, E>. Map errors to domain-specific error types.
**3. Gap Inventory**
Use `prompts/02-gap-inventory.md`:
```bash
# Creates migration/inventory.tsv
A flat table of every site where the target language demands explicit decisions:
- Ownership annotations
- Lifetime parameters
- Nullability markers
- Interface contracts
- Concurrency primitives
Implementers grep it; nobody reads it cover-to-cover.
Generate manifest:
python migration-kit/scripts/make_manifest.py \
migration/depmap.json \
> migration/manifest.tsv
Step 2: Stress-Test Rules
Before any fan-out, run prompts/03-stress-test.md:
Bakeoff:
- Two translators in separate contexts
- One follows rulebook, one doesn't know it exists
- Diff inspector turns every difference into a verdict on a rule
- Amendments queued for human approval, never self-applied
Pilot:
- Run production pipeline exactly as Step 3 will
- Pick 3-5 nasty files (deepest dependencies, most complex)
- Grade on obedience to rules, not output quality
- Install
.claude/settings.json BEFORE this pilot:
cp migration-kit/templates/settings.json ./.claude/settings.json
Critical: settings.json must exist before Step 2 pilot and remain active through Step 4. See templates/settings.README.md for the rationale.
Step 3: Translate Everything
Install queue runner:
chmod +x migration-kit/scripts/queue_runner.mjs
Kick off with prompts/04-translation-kickoff.md:
Translate the codebase using the established rulebook.
Manifest: migration/manifest.tsv
Rulebook: migration/RULEBOOK.md
Output directory: migration/translated/
Settings: .claude/settings.json (denies active)
For each file in manifest order:
1. Implementer translates (follows rulebook exactly)
2. Adversarial reviewer 1 (checks rule compliance)
3. Adversarial reviewer 2 (checks mistake class X)
4. Fixer (applies amendments)
5. Write to migration/translated/<target_path>
Don't run the compiler yet. Settings bans:
- Test execution
- Build commands
- File operations outside
migration/translated/
The queue runner processes migration/manifest.tsv:
node migration-kit/scripts/queue_runner.mjs \
--manifest migration/manifest.tsv \
--output migration/translated \
--rulebook migration/RULEBOOK.md
Step 4: Compile
Use prompts/05-survey-build.md to run one survey build:
./migration-kit/scripts/build_daemon.sh \
migration/translated \
"cargo build --all 2>&1" \
migration/build-output
The daemon:
- Watches
migration/translated/
- Reruns build on changes
- Emits numbered error files:
migration/build-output-r1.txt, r2.txt, etc.
- Slices errors by module (leaves to root)
Fixers work without compiler access:
Fix compilation errors from migration/build-output-r{N}.txt
Rules:
- No running builds yourself (daemon owns it)
- Read numbered error file
- Fix issues in dependency order
- Write fixes to migration/translated/
- Daemon reruns automatically
- Consume next numbered file
Repeat until clean build.
If target typecheck is cheap (TypeScript, Go):
This step dissolves into Step 3 — edit .claude/settings.json to remove typecheck denies, run typechecker inside each unit's loop instead of batching.
Step 5: Run It
./migration/translated/bin/hello
./migration/translated/bin/app --version
./migration/translated/bin/app test-basic-operation
Step 6: Match Behavior
Your judge from Step 0b decides the gate:
If tests hit public surface (CLI/API):
pytest migration/translated/tests/
pytest tests/
If using parity harness:
python migration/parity_harness.py --target old > old_output.json
python migration/parity_harness.py --target new > new_output.json
diff old_output.json new_output.json
Done gate:
- Every parity test passes
- Original suite re-run on original code with zero inherited failures
- Both counts documented in final report
After parity, use prompts/06-post-parity.md:
Burn down deferred markers:
- BUG(port): [count from grep]
- TODO(port): [count from grep]
- PERF(port): [count from grep]
Each fix:
- Own flagged commit
- Proved by parity re-run
- Documents why it was deferred
Configuration
Settings File (.claude/settings.json):
{
"deny_operations": {
"test_execution": true,
"build_commands": true,
"file_operations_outside_workspace": true
},
"migration": {
"workspace": "migration/translated",
"rulebook": "migration/RULEBOOK.md",
"manifest": "migration/manifest.tsv"
}
}
Timeline:
- Installed before Step 2 pilot
- Active through Step 4
- Test denies re-activated for Step 6 fix loops
- If Step 4 dissolves into Step 3 (cheap typecheck), remove typecheck denies
Real Code Example: Python → Rust Migration
Original Python (src/parser.py):
class ConfigParser:
def __init__(self, path):
self.path = path
self.data = {}
def load(self):
try:
with open(self.path) as f:
self.data = json.load(f)
except FileNotFoundError:
self.data = {"default": True}
return self.data
def get(self, key, default=None):
return self.data.get(key, default)
Rulebook Entry:
## Class Translation
**Python classes → Rust structs + impl blocks**
- `__init__` → `new()` constructor
- Instance methods → `&self` methods
- Exceptions → Result<T, E>
- Optional args → Option<T>
Translated Rust (migration/translated/src/parser.rs):
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use serde_json;
#[derive(Debug)]
pub enum ConfigError {
ReadFailed(PathBuf, std::io::Error),
ParseFailed(serde_json::Error),
}
pub struct ConfigParser {
path: PathBuf,
data: HashMap<String, serde_json::Value>,
}
impl ConfigParser {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
path: path.as_ref().to_owned(),
data: HashMap::new(),
}
}
pub fn load(&mut self) -> Result<&HashMap<String, serde_json::Value>, ConfigError> {
match fs::read_to_string(&self.path) {
Ok(contents) => {
self.data = serde_json::from_str(&contents)
.map_err(ConfigError::ParseFailed)?;
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
.data.(.(), serde_json::json!());
}
(e) => (ConfigError::(.path.(), e)),
}
(&.data)
}
(&, key: &) <&serde_json::Value> {
.data.(key)
}
}
Common Patterns
Resumable Workflows
Every queue is defined by what exists on disk:
node migration-kit/scripts/queue_runner.mjs --manifest migration/manifest.tsv
Stopping is free. Resuming is re-invocation, not recovery.
Sign-Off Gates
Prompts end with gates, not auto-continue:
=== GATE ===
Sign-off required to proceed to Step 3.
Evidence:
- Bakeoff diff: migration/bakeoff-diff.md
- Pilot results: migration/pilot-results.md
- Proposed amendments: migration/rule-amendments.md
Your approval kicks off Step 3 translation fan-out.
Your sign-off = starting the next prompt.
Adversarial Review
Each unit gets 3 reviewers with different mandates:
reviewers = [
{"role": "rule_compliance", "rejects_on": "any rulebook deviation"},
{"role": "safety", "rejects_on": "memory unsafety, data races"},
{"role": "performance", "rejects_on": "allocations in hot path"},
]
Dependency Map Usage
import json
depmap = json.load(open("migration/depmap.json"))
file_deps = depmap["files"]["src/module.py"]["dependencies"]
all_translated = all(
os.path.exists(f"migration/translated/{dep}")
for dep in file_deps
)
Troubleshooting
"Feasibility prompt says don't migrate"
Don't migrate. Valid outcomes:
- Stay on current language
- Incremental adoption (TypeScript superset model)
- Rewrite from scratch (if redesigning heavily)
The kit is for total, structure-preserving migrations. If that's not your case, the ROI isn't there.
"Tests fail but they also fail on original code"
These are inherited failures, not regressions:
pytest tests/test_config.py::test_parse
pytest tests/test_config.py::test_parse
The done-gate explicitly requires: "original suite re-run on original code with zero inherited failures."
"Parity harness reports all divergences"
Debug the referee first. In early testing, all reported divergences traced to comparator bugs (whitespace handling, JSON.stringify converting NaN to null), not the port.
def test_referee_catches_bugs():
broken_impl = introduce_bug(correct_impl)
assert referee.compare(broken_impl, correct_impl) != "PASS"
"Build errors repeat across modules"
Indict the rule, not the code:
Recurring error: "lifetime parameter required but not in rulebook"
Action:
1. Add to migration/rule-amendments.md
2. Update RULEBOOK.md with lifetime rules
3. Re-run affected translations (grep manifest for pattern)
4. DO NOT fix individual files without fixing the rule
"Dependency map has cycles"
File-level cycles are rare but package-level cycles are common:
python migration-kit/scripts/depmap_python.py ./src --check-cycles
For package cycles: Break them in the target language's module system first (Rust: pub(crate), careful re-exports), then translate.
"Queue runner stuck on one file"
tail -f migration/queue_runner.log
echo "src/broken.py" >> migration/skip_list.txt
node migration-kit/scripts/queue_runner.mjs --skip-list migration/skip_list.txt
"Settings.json never installed, Step 3 ran unbounded"
This happened in early testing. The dissolve (removing typecheck denies) silently proceeded with no guardrails.
Fix:
cp migration-kit/templates/settings.json ./.claude/settings.json
Prevention: Prompts 03 and 04 now verify settings.json exists and stop without it.
When NOT to Use This Kit
- Incremental migrations (JavaScript → TypeScript): Target is a superset, adopt file-by-file
- Greenfield rewrites: No existing codebase to preserve structure from
- Small codebases: < 50 files, manual translation is faster
- Heavy redesign: If you're changing architecture radically, parts of this kit become invalid (bakeoff, file-level units)
Use this kit when:
- The move is total (old language gets deleted)
- Every file must cross
- You're preserving structure (same data flow, new syntax)
- Scale justifies automation (100+ files)
Environment Variables
Scripts expect:
export MIGRATION_WORKSPACE=./migration
export SOURCE_ROOT=./src
export TARGET_ROOT=./migration/translated
export CLAUDE_SETTINGS=./.claude/settings.json
No API keys needed — this kit orchestrates Claude Code workflows via prompts, not API calls.