Skip to content

Large-scale refactoring with Codex splits one behavior-preserving change across hundreds of files into a written plan, an expand step on one branch, and independent batches that each run in their own Git worktree and merge one at a time. Unchanged HTTP tests, a ratchet counting the old API down to zero, and CI gates prove each batch.

Your team has agreed to move the API from Express to Hono and to rename every getUserById to findUser before the next release. That is 300 files across a dozen route groups. One long-lived branch means a code freeze and a review nobody can finish. This page is for the developer who runs the refactor with Codex and the tech lead who has to approve each batch without reading every changed line.

This page covers refactors of code that already has tests and keeps its behavior. For legacy code without tests, start with legacy code modernization with Codex. For the mechanics of a single worktree (what it carries over, cleanup), see isolated Codex tasks with Git worktrees.

What you’ll walk away with from a Codex refactor

Section titled “What you’ll walk away with from a Codex refactor”
  • A job-to-surface map: what a codemod does, what an interactive session does, what a worktree batch does, and what goes to Codex cloud.
  • The expand, migrate, contract order that keeps main deployable on every day of the refactor.
  • A ratchet script and a route inventory that prove “nothing changed” mechanically.
  • Five copy-paste prompts: the plan, the expand step, a batch, a codemod rename, and the contract cleanup.
  • A per-batch evidence checklist a tech lead can sign, and recovery steps for the failures that recur.

Which Codex surface does each refactoring job?

Section titled “Which Codex surface does each refactoring job?”

Commands below were checked against codex-cli 0.157.1 on 2026-09-26.

JobSurfaceWhy there
Plan the batchesInteractive codex in plan mode (/plan)You review the dependency graph before any file changes
Pure renames (getUserById to findUser)A codemod script that Codex writes and runsA rename is deterministic; a script does not skip call sites or improvise
Expand: shared middleware, adaptersInteractive codex on one base branchShared code needs one owner and your attention
Migrate route groupsOne git worktree add checkout per batch, one codex session eachBatches run in parallel without editing each other’s files
Contract: remove Expresscodex cloud exec --env ENV_ID --branch BRANCHA long, mechanical task that needs no laptop
First review of each batchcodex exec review --base BRANCHA reviewer model reads the diff before a person does

Step 1: Plan the refactor and write it to a file

Section titled “Step 1: Plan the refactor and write it to a file”

Parallel batches fail on shared files, not on hard code. The plan exists to find every shared file before any batch starts. Start codex in the repository, switch to plan mode with /plan, and paste:

Read the file-ownership list, not the prose. If two batches both need src/lib/session.ts, move it to the expand phase now. Commit the plan file on a base branch, refactor/hono-base, because every batch prompt points to it.

Step 2: Set the ratchet and freeze the oracle

Section titled “Step 2: Set the ratchet and freeze the oracle”

Before any code moves, decide what “behavior unchanged” means in commands. Two artifacts do it.

The oracle. HTTP-level tests in tests/http/ send real requests to the app and assert status codes, headers and bodies. They do not care which framework answers, so they must pass unchanged in every batch. The one file a batch may touch is the bootstrap that builds the app under test.

The ratchet. A script counts the files that still use the old API. CI fails if the count rises above the committed baseline, and each batch lowers the baseline. The Express half counts only src/routes/: the expand phase keeps an Express shell (src/app.ts, src/middleware/express/) that mounts Hono until the contract phase deletes it, so a count across all of src/ could never reach zero before then:

#!/usr/bin/env bash
# scripts/refactor-ratchet.sh: run in CI on every pull request
set -euo pipefail
count=$( { grep -rlE "from ['\"]express['\"]|require\(['\"]express['\"]\)" src/routes/ || true; grep -rl "getUserById" src/ || true; } \
| sort -u | wc -l | tr -d ' ')
baseline=$(cat .refactor-baseline)
echo "files on the old API: $count (baseline $baseline)"
if [ "$count" -gt "$baseline" ]; then
echo "old API usage went up"; exit 1
fi

The pattern matches both quote styles and require(). Before the first batch, run the script and confirm the count is above zero: a count of zero on day one means the grep cannot see your imports, and every later check would pass without proving anything.

Add a route inventory too: scripts/print-routes.sh prints the method and path of every registered route, sorted. Commit its output as docs/refactor/routes.txt. After each batch the output must be identical, which catches a route that was dropped or renamed without anyone reading a handler.

Step 3: Build the expand phase on one branch

Section titled “Step 3: Build the expand phase on one branch”

The expand phase lets both frameworks serve at once, so every batch merges into a deployable app. @hono/node-server (2.1.1 on npm, 2026-09-26) exports getRequestListener, which turns a Hono app into a Node request handler that Express can mount under a prefix. Stay on refactor/hono-base and paste:

One trap in the mount: Express strips the mount prefix from req.url before it calls a mounted handler, so under /auth the Hono app sees /login, not /auth/login, and a Hono route defined with its full path returns 404. Define Hono routes relative to their mount prefix, or mount the listener without a path (app.use((req, res) => listener(req, res))) so Hono sees full paths; register that catch-all after every Express route, as the last middleware, because it answers every request that reaches it. The HTTP tests and the route inventory diff catch either mistake.

Merge this branch to main before the batches start. From here on, main runs both frameworks and stays deployable.

Step 4: Run each batch in its own worktree

Section titled “Step 4: Run each batch in its own worktree”

Create one named branch and checkout per route group from the merged base. Run in your terminal, once per batch:

Terminal window
git fetch origin
git worktree add -b refactor/hono-auth ../api-hono-auth origin/main
cd ../api-hono-auth && npm ci && codex

codex --worktree creates a managed worktree instead, but on a detached HEAD; for work that must become a named pull request, git worktree add -b is the simpler form. Paste the same prompt into every session and change only the group name:

To start the batches from a script instead of by hand, codex exec -C DIR runs the same prompt non-interactively in each checkout; the Codex multi-agent workflows page has a fan-out script and a cap on how many lanes to run at once.

Rename across the codebase with a codemod, not by hand

Section titled “Rename across the codebase with a codemod, not by hand”

A rename touches hundreds of call sites and needs no judgment, so the agent’s job is to write the tool, not to be the tool. An agent editing 400 call sites by hand can skip one; a script either handles every match or fails on it. Run it in its own worktree, and at a point where no batch is open: as part of the expand phase before the batches start, or after the last batch merges and before the contract phase. Never run it alongside the batches, because it edits files the plan assigns to individual batches.

Reviewers then read a 40-line script instead of a 400-file diff, and can rerun it on a newer base if the branch goes stale.

Merge in the order the plan gives, one batch per pull request, and let CI run between merges. After each merge, rebase the open batches onto the new main and rerun their gates: a middleware change in one batch can break another batch’s error bodies. Ask Codex in the rebased worktree:

Rebase this branch onto origin/main. Resolve conflicts by keeping main's version of any file
this batch does not own. Then rerun every command from the batch prompt and report the results.

Step 6: Run the contract phase in Codex cloud

Section titled “Step 6: Run the contract phase in Codex cloud”

When the ratchet reads zero, no route and no call site uses the old API. What remains is the Express shell the plan lists under CONTRACT (src/app.ts, src/middleware/express/), and removing it is mechanical. Codex cloud clones from your remote, so push first and pass the branch explicitly. Run in your terminal, where ENV_ID is your Codex cloud environment ID or label:

Terminal window
git fetch origin
git push origin origin/main:refs/heads/refactor/hono-contract
codex cloud exec --env ENV_ID --branch refactor/hono-contract < docs/refactor/contract.md

codex cloud exec reads the prompt from standard input when you pass no argument, and prints the task URL; codex cloud diff and apply accept the URL or the ID at its end. The git push command creates the remote branch without switching your working copy, so the worktree below gets a named branch to commit the applied diff to. Inspect and apply the result in a clean checkout, not in your working copy:

Terminal window
codex cloud diff TASK_URL
git fetch origin
git worktree add -b refactor/hono-contract-apply ../api-hono-contract origin/refactor/hono-contract
cd ../api-hono-contract && codex cloud apply TASK_URL
npm ci && npm run typecheck && npm test

--attempts 2 to 4 asks for several independent attempts; for a mechanical cleanup one attempt is usually enough. See when best-of-N is worth paying for.

How do you verify a refactoring batch without reading every line?

Section titled “How do you verify a refactoring batch without reading every line?”

A refactor is correct when observable behavior did not move. Each batch proves that with the same evidence, collected by commands rather than by a reader:

  1. The oracle did not move. git diff --stat origin/main...HEAD -- tests/http/ is empty, or lists only tests/http/setup.ts.

  2. The gates pass in CI, not only in the agent’s report: npm run typecheck, npm run lint and npm test.

  3. The ratchet fell. scripts/refactor-ratchet.sh reports fewer files, and .refactor-baseline was lowered in the same pull request.

  4. The route inventory is identical. The diff against docs/refactor/routes.txt is empty.

  5. An automated reviewer read the diff first. In the batch’s worktree, run:

    Terminal window
    codex exec review --base origin/main

    --base cannot be combined with custom review instructions (checked in codex-cli 0.157.1: the command exits with the argument '--base <BRANCH>' cannot be used with '[PROMPT]'). For a custom focus, run /review in the batch’s interactive session, or pipe the diff into a plain codex exec prompt, which appends standard input to the prompt:

    Terminal window
    git diff origin/main...HEAD | codex exec "Flag any behavior change: status codes, headers, error bodies, middleware order, or edits outside this batch's files. Say NO FINDINGS if there are none."

    On a pull request, @codex review does the same. See Codex code review.

  6. A named person signs off. The batch owner merges on the evidence above. Authentication and billing batches also get a human read of the handlers, as reading evidence instead of code sets out.

  7. Production can undo it. Each batch ships as its own deploy, so a regression rolls back one route group, not the whole refactor.

Put the evidence in the pull request description so the approver does not have to rerun anything; the evidence bundle an agent’s pull request must prove gives the format.

SymptomCauseRecovery
Two batches conflict on the same fileThe plan missed a shared importMerge the first batch, rebase the second, and move the file to the expand phase in the plan. Rerun the planning prompt’s shared-file check before the next wave
A batch is green because a test changedThe agent weakened an assertion to match new behaviorThe empty tests/http/ diff blocks the merge. Rerun with “fix the implementation, never the test” at the top of the prompt
Tests pass in each worktree but fail after two mergesEach batch ran against a base without the other’s changesRebase open batches after every merge and rerun their gates, as in step 5
Error responses change shape after migrationHono’s default error handling differs from the Express error middlewareAdd a characterization test for the error body, then port the error handler in the expand phase, not in a batch
The rename missed call sites or renamed the wrong functionThe agent edited by hand, or matched on text instead of typesRevert, and rerun the codemod prompt: a type-aware script, dry-run count first
The ratchet count goes up on mainA teammate’s feature branch added new Express routesCI blocks it; ask them to write the route in Hono, or raise the baseline deliberately in a reviewed commit
The cloud task cannot find the Hono appThe branch was not pushed, or --branch was omitted and the task ran on your current local branchPush first and pass --branch explicitly
Commits vanished with a removed worktreeA managed worktree on a detached HEAD was removed before its commits reached a branch; its reflog went with itBefore git gc runs, run git fsck --lost-found, find yours with git show SHA, then git branch rescue/BATCH SHA. Prevent it by always creating a named branch (git worktree add -b); see the Codex worktrees guide

The same workflow in other tools: refactoring with Claude Code and refactoring strategies in Cursor.