Skip to content

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 /rewind cannot undo, with the recovery for each.

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.

MigrationBulk work done byProof that a phase is safeDeep dive
Schema change on a live tableClaude Code writes phase files; your migration tool applies themLock lint, up-down-up on a scratch database, a parity query that returns 0Database migration patterns
Framework or library major versionThe vendor’s codemod first, then Claude Code for what it missedType check, full test suite, a grep that finds no old APIBatch operations
ORM major versionClaude Code, from the vendor’s upgrade guideThe SQL the ORM emits, diffed before and afterORM patterns
Third-party API versionClaude Code, behind a feature flagContract tests against recorded v2 responses, then a shadow comparisonProgressive 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.

  1. Work in a worktree. claude -w users-name-split (--worktree in claude --help 2.1.283) creates a git worktree and starts the session inside it, so a failed attempt is a directory you delete.

  2. Point the shell at a scratch database. Claude Code runs commands with your environment. With v2.1.283 on the latest channel, auto mode is the starting permission mode for interactive terminal sessions on the models it supports, so a classifier, not you, approves most commands. If DATABASE_URL points at staging, one migrate deploy reaches it. Start a throwaway Postgres and export its URL before you run claude. 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 work
    claude mcp add -s project postgres -e 'DATABASE_URI=${STAGING_DATABASE_URL}' \
    -- uvx postgres-mcp --access-mode=restricted
  3. Commit the baseline. Checkpoints and /rewind restore only edits made through Claude’s file-editing tools. A codemod, sed or mv run through Bash is not tracked. A commit before each phase is your real undo.

  4. Write the migration rules into CLAUDE.md. Every session, subagent and /batch worker 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.

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 window
# Terminal or CI: lint every migration file this branch adds
FILES=$(git diff --name-only --diff-filter=A origin/main -- 'db/migrations/*.sql')
[ -z "$FILES" ] || npx squawk-cli@2.66.0 $FILES

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 window
# Terminal, in the worktree, after the baseline commit
npx @expressjs/codemod upgrade src
git 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 outputGate that proves itWho signs off
MIGRATION.md planEvery phase names its deploy, gate and rollback; the backfill follows dual writeTech lead, before plan mode is approved
Expand or contract fileSquawk passes; up, down, up on a scratch database with an empty schema diff; CI repeats itSchema owner, through CODEOWNERS on the migrations directory
BackfillParity query returns 0 on seeded edge cases, then on production through a read-only roleMigration owner, with both counts in the pull request
Framework upgradeType check, full suite, and a grep for removed APIs that returns nothingCI
ORM upgradeSQL diff before and after, each changed statement explainedReviewer of the pull request
API version moveContract tests on recorded responses; shadow-mode difference log at zeroService 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:

Terminal window
# 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.json
jq -e '.subtype == "success" and .is_error == false' migration-review.json > /dev/null

The 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.

The same work in the other tools: codebase modernization in Codex, database work in Codex and migrations in Cursor.