Database and code migrations with Claude Code
A migration with Claude Code is a sequence of separately shipped phases (expand, backfill, switch, contract for a schema; codemod, agent fixes, verification for a framework upgrade) that Claude Code plans, writes and tests on a disposable database or branch. The qualifier that matters: each phase ships only after a gate proves it, and CI, not the agent, applies it.
This page is for developers who run the migration in the terminal and for tech leads who approve it. Your users table has 12 million rows and name has to become first_name and last_name. Express 4 has to become Express 5 across 180 route files. Your payment provider switches off its v1 API at the end of the quarter. Claude Code can write every file for all three in an afternoon, and one of those files can still take production down.
What you’ll walk away with from migrations in Claude Code
Section titled “What you’ll walk away with from migrations in Claude Code”- A session setup that keeps the agent on a scratch database and a branch you can throw away.
- A plan-mode inventory, built by subagents, that tells you the blast radius before any file changes.
- Copy-paste prompts for an expand-contract schema change, a resumable backfill, a framework upgrade, an ORM upgrade and an API version move behind a flag.
- A gate for each kind of migration, so you check evidence rather than every line the agent wrote.
- The ways migrations break in Claude Code specifically, including what
/rewindcannot undo, with the recovery for each.
Which kind of migration are you running?
Section titled “Which kind of migration are you running?”The workflow is the same for every migration: inventory, plan in phases, execute one phase, prove it, then ship it. What changes is the tool that does the bulk work and the evidence that proves a phase.
| Migration | Bulk work done by | Proof that a phase is safe | Deep dive |
|---|---|---|---|
| Schema change on a live table | Claude Code writes phase files; your migration tool applies them | Lock lint, up-down-up on a scratch database, a parity query that returns 0 | Database migration patterns |
| Framework or library major version | The vendor’s codemod first, then Claude Code for what it missed | Type check, full test suite, a grep that finds no old API | Batch operations |
| ORM major version | Claude Code, from the vendor’s upgrade guide | The SQL the ORM emits, diffed before and after | ORM patterns |
| Third-party API version | Claude Code, behind a feature flag | Contract tests against recorded v2 responses, then a shadow comparison | Progressive delivery |
Whole-system rewrites (a language change, a monolith split) are a program, not a migration. They need characterization tests and slice plans; see legacy modernization with agents.
Set up the session before the migration starts
Section titled “Set up the session before the migration starts”Four steps keep a migration reversible. Do them in this order, because each one assumes the previous one.
-
Work in a worktree.
claude -w users-name-split(--worktreeinclaude --help2.1.283) creates a git worktree and starts the session inside it, so a failed attempt is a directory you delete. -
Point the shell at a scratch database. Claude Code runs commands with your environment. With v2.1.283 on the
latestchannel, auto mode is the starting permission mode for interactive terminal sessions on the models it supports, so a classifier, not you, approves most commands. IfDATABASE_URLpoints at staging, onemigrate deployreaches it. Start a throwaway Postgres and export its URL before you runclaude. Database work in Claude Code has the container command, the deny rules for your migration tool and the read-only Postgres MCP Pro server for looking at staging:Terminal window # Terminal, repo root: read-only Postgres MCP Pro for staging, from Database workclaude mcp add -s project postgres -e 'DATABASE_URI=${STAGING_DATABASE_URL}' \-- uvx postgres-mcp --access-mode=restricted -
Commit the baseline. Checkpoints and
/rewindrestore only edits made through Claude’s file-editing tools. A codemod,sedormvrun through Bash is not tracked. A commit before each phase is your real undo. -
Write the migration rules into
CLAUDE.md. Every session, subagent and/batchworker reads them. See the memory system for placement.## Migrations- Never edit a migration file that has been applied anywhere. Write a new one.- One phase per migration file: expand, backfill, contract are separate files and separate deploys.- Every file starts with SET lock_timeout = '5s' and names the lock each statement takes.- The only database you may touch is $DATABASE_URL (local scratch). Never run migrate deploy.- Do not edit tests to make a migration pass. Report the failing test instead.
To make the first rule mechanical instead of a request, add a PreToolUse hook that refuses Edit and Write on applied migration directories. Claude Code hooks has a tested protect-paths.sh you can adapt.
Map the blast radius in plan mode
Section titled “Map the blast radius in plan mode”Plan mode lets Claude Code read and propose without editing. Press Shift+Tab until the mode indicator shows plan mode, or start with claude --permission-mode plan. Ask for an inventory from parallel Explore subagents: each searches one area and returns a summary, so the main session keeps its context for the plan.
MIGRATION.md becomes the contract for the rest of the work. Review it for three things: the backfill comes after every instance dual-writes, the contract phase is a separate release, and every phase names its rollback. Approve the plan, then run each phase as its own task. If the session’s context fills up, run /clear and start the next phase with “Read MIGRATION.md and do phase 3”.
Change a live schema with expand and contract
Section titled “Change a live schema with expand and contract”Expand-contract never changes a column in place. You add the new shape, write both shapes, copy the old data, switch reads, and drop the old shape a release later. Old and new application versions both work at every step, so a rollback between phases is a redeploy, not a restore.
Prove the expand file on the scratch database before anything else: apply it, apply the down file, compare pg_dump --schema-only output before and after, and apply it again. Database work in Claude Code has that up-down-up script. An empty schema diff proves the rollback without anyone reading the SQL. Then lint the file for dangerous locks with Squawk, the PostgreSQL migration linter; it exits non-zero on an error, so the same line works as a CI step:
# Terminal or CI: lint every migration file this branch addsFILES=$(git diff --name-only --diff-filter=A origin/main -- 'db/migrations/*.sql')[ -z "$FILES" ] || npx squawk-cli@2.66.0 $FILESBackfill a large table without locking it
Section titled “Backfill a large table without locking it”A single UPDATE over 12 million rows holds its locks for one long transaction and bloats the table. Ask for a batch job that can stop and resume.
The order is the whole point. If the backfill starts before every instance dual-writes, an old instance can update name after the backfill passed that row, and the new columns go stale without any error. Lock-by-lock guidance, the Squawk lint gate and the rollback per phase are on database migration patterns.
Upgrade a framework: codemod first, agent second
Section titled “Upgrade a framework: codemod first, agent second”A vendor codemod is deterministic and cheap, so run it before you spend tokens. Claude Code then handles what the codemod does not cover, reading the vendor’s migration guide rather than its own memory of the API.
For Express 4 to 5, the Express team’s @expressjs/codemod (0.0.5) ships four transforms: the magic "back" redirect, pluralized method names, the v4 deprecated signatures, and req.param(). npm marks the package as deprecated (checked 2026-09-26) and points to the Express codemods in the codemod.com registry (scope expressjs) instead. This page keeps the npm package because it still runs and covers these four transforms; treat the registry as the place to look for newer recipes. Its upgrade command has no dry run, so run it on a clean tree and let git diff be the preview; git restore . undoes it:
# Terminal, in the worktree, after the baseline commitnpx @expressjs/codemod upgrade srcgit add -A && git commit -m "Express 5: apply official codemod"Route path strings are a common gap: Express 5 parses them with path-to-regexp 8 (through router 2), so wildcard and optional-segment syntax needs changes the codemod does not make.
When the change touches hundreds of files across many directories, /batch splits it into 5 to 30 units, each in its own worktree with its own pull request. Give it the verification recipe from steps 3 and 4 above; batch operations covers the prompt and the merge order.
Upgrade an ORM by diffing the SQL it emits
Section titled “Upgrade an ORM by diffing the SQL it emits”An ORM upgrade can pass every test and still change behaviour: a different default ordering, NULL handling in a filter, or a new query shape that skips an index. The type checker cannot see any of that. The SQL can.
The SQL diff is what the reviewer reads. A handful of changed statements, each explained, is a review of the whole upgrade; the call-site edits behind them are covered by the type check and the tests.
Move a third-party API version behind a flag
Section titled “Move a third-party API version behind a flag”An API version move changes code whose other side you do not control, so ship it dark first. Record real v2 responses in the provider’s sandbox, build contract tests on them, and switch traffic with a flag.
Turn shadow mode on in production for read-only calls, watch the difference log reach zero, then move write traffic with the flag in steps. Progressive delivery covers the rollout and the automatic rollback trigger.
How do you know the migration is right without reading every line?
Section titled “How do you know the migration is right without reading every line?”Each phase leaves evidence behind. The reviewer reads the evidence, and spends line-by-line attention only where no gate can judge.
| Phase output | Gate that proves it | Who signs off |
|---|---|---|
MIGRATION.md plan | Every phase names its deploy, gate and rollback; the backfill follows dual write | Tech lead, before plan mode is approved |
| Expand or contract file | Squawk passes; up, down, up on a scratch database with an empty schema diff; CI repeats it | Schema owner, through CODEOWNERS on the migrations directory |
| Backfill | Parity query returns 0 on seeded edge cases, then on production through a read-only role | Migration owner, with both counts in the pull request |
| Framework upgrade | Type check, full suite, and a grep for removed APIs that returns nothing | CI |
| ORM upgrade | SQL diff before and after, each changed statement explained | Reviewer of the pull request |
| API version move | Contract tests on recorded responses; shadow-mode difference log at zero | Service owner, before the flag moves |
Two things are always read by a human: the contract phase that drops data, and any step the plan marks irreversible. Migrations are one of the few change classes where that reading still pays, as reading evidence instead of code explains. Everything else goes into the evidence bundle of the pull request.
To repeat the check in CI without a person, run a read-only review of the new migration files with claude -p. It starts in Manual mode and cannot answer permission prompts, so --allowedTools is its whole tool surface, and --json-schema makes the output a file a later step can check:
# CI job on a trusted branch (push to main or a same-repo pull request)FILES=$(git diff --name-only --diff-filter=A origin/main -- 'db/migrations/*.sql')claude -p "Review these new migration files: $FILES. For every statement, name the lock it takes, whether the paired down migration reverses it, and whether it can lose data." \ --bare --setting-sources "" --strict-mcp-config \ --allowedTools "Read Grep Glob" \ --output-format json \ --json-schema "$(cat .github/migration-review.schema.json)" \ > migration-review.jsonjq -e '.subtype == "success" and .is_error == false' migration-review.json > /dev/nullThe schema file describes an array of {file, statement, lock, reversible, loses_data} objects; a follow-up step fails the job when any entry has loses_data: true without a human approval label. The jq -e line fails the job when the run did not finish, so a crashed or capped review never passes as an empty one. --bare --setting-sources "" --strict-mcp-config keeps the branch’s own hooks, settings and .mcp.json from loading next to ANTHROPIC_API_KEY; --bare authenticates only with that key. On a pull request the workflow, the schema file and the follow-up step come from the pull request itself, so put .github/ under CODEOWNERS with required review, or a pull request can weaken the check in the same diff. Run it only on trusted branches, and never on fork code under pull_request_target: the hooks and MCP servers in the contributor’s .claude/ would run with the job’s secrets. CI/CD with Claude Code has the safe workflow for that case.
What breaks when you migrate with Claude Code
Section titled “What breaks when you migrate with Claude Code”/rewind did not undo the codemod. Checkpoints track only edits made through Claude’s file-editing tools, not files changed by a Bash command. Recover with git: git restore . back to the baseline commit, or git reset --hard in the worktree. Prevent it with a commit before every phase.
The backfill runs out of memory or slows the primary. The script loaded rows into memory or ran without pauses. Stop it; the first_name IS NULL filter means nothing is lost. Ask Claude Code to lower the batch size, stream with keyset pagination and log the last id, then resume with --start-after.
The sync trigger recurses or doubles write latency. A BEFORE row trigger that only assigns NEW columns does not recurse. If the agent wrote an AFTER trigger that updates its own table, it fires again: rewrite it as a BEFORE trigger and keep the pg_trigger_depth() guard. Either way, measure insert latency on the scratch database before and after the expand file. If the cost is too high, drop the trigger and rely on application dual write, which means the backfill must wait until no old instance is running.
The parity query is not zero after the backfill. An instance on the old version wrote name only, or the split function disagrees with itself on edge cases. Confirm every instance runs the dual-write version, run a repair batch that filters on the mismatch instead of IS NULL, and check the review table. Do not ship the contract phase until the count is 0.
The migration passes on seeded data and fails in production. Production has values your seed does not: NULL where the schema allows it, empty strings, very long strings, unusual Unicode. Ask Claude Code for a pre-flight query that counts each of those in the affected columns, run it through the read-only role, and add the cases to the seed.
Claude Code edited an applied migration or a test to get to green. The session had write access to both. Revert the file from git, restate the rule, and make it mechanical with the protect-paths.sh hook and an Edit deny rule for test files. See protecting the oracle.
A long upgrade loses the thread halfway. The context filled with test output and the early decisions were summarized away. Keep state in MIGRATION.md, ask Claude Code to tick off each phase in it, and start each phase in a fresh session.
Where to go next with migrations
Section titled “Where to go next with migrations”The same work in the other tools: codebase modernization in Codex, database work in Codex and migrations in Cursor.