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
# Clone inside or adjacent to the repo you're migrating
git clone https://github.com/anthropics/code-migration-kit-with-claude-code ./migration-kit
# Install as a Claude Code skill (optional)
cp -r migration-kit/skill ~/.claude/skills/code-migration
# Copy the operating manual to target repo
cp migration-kit/CLAUDE.md ./CLAUDE.md
# Create migration workspace
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
# For Python projects
python migration-kit/scripts/depmap_python.py ./src > migration/depmap.json
# For JavaScript/TypeScript
node migration-kit/scripts/depmap_js.mjs ./src > migration/depmap.json
# For C/C++ headers
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
# Copy template
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:
# After dependency map exists
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.jsonBEFORE 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:
# Make executable
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:
// Queue runner handles resume automatically
// Stop anytime, restart with same command
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:
# Start build daemon (human runs once)
./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
# Hello world
./migration/translated/bin/hello
# Smallest end-to-end command
./migration/translated/bin/app --version
# Smoke tests (cheap proofs before expensive ones)
./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):
# Run new tests against new code
pytest migration/translated/tests/
# Triage failures by running against old code
pytest tests/ # Original suite on original code
# Classify: regression / inherited / environment
# Burn down regression queue
If using parity harness:
# Run harness against both
python migration/parity_harness.py --target old > old_output.json
python migration/parity_harness.py --target new > new_output.json
# Diff results
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 => {
self.data.insert("default".to_string(), serde_json::json!(true));
}
Err(e) => return Err(ConfigError::ReadFailed(self.path.clone(), e)),
}
Ok(&self.data)
}
pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
self.data.get(key)
}
}
Common Patterns
Resumable Workflows
Every queue is defined by what exists on disk:
# Stop anytime (Ctrl+C)
# Resume by re-running same command
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:
# In translation loop
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
# Check if file is ready to translate
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:
# Classify each failure
pytest tests/test_config.py::test_parse # On new code: FAIL
pytest tests/test_config.py::test_parse # On old code: FAIL → INHERITED
# Document in migration/inherited-failures.md
# Don't block parity gate on these
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.
# Validate referee against deliberately broken code
# BEFORE trusting its verdicts
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:
# Check both levels
python migration-kit/scripts/depmap_python.py ./src --check-cycles
# Output shows:
# File cycles: 0
# Package cycles: 3 (details in depmap.json)
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"
# Check migration/queue_runner.log
tail -f migration/queue_runner.log
# Skip problematic file temporarily
echo "src/broken.py" >> migration/skip_list.txt
node migration-kit/scripts/queue_runner.mjs --skip-list migration/skip_list.txt
# Come back to it after understanding the pattern
"Settings.json never installed, Step 3 ran unbounded"
This happened in early testing. The dissolve (removing typecheck denies) silently proceeded with no guardrails.
Fix:
# Install now
cp migration-kit/templates/settings.json ./.claude/settings.json
# Re-run Step 2 pilot to validate denies work
# Then resume Step 3
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.