Large-scale refactoring with Codex worktrees
PLLarge-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
maindeployable 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.
| Job | Surface | Why there |
|---|---|---|
| Plan the batches | Interactive 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 runs | A rename is deterministic; a script does not skip call sites or improvise |
| Expand: shared middleware, adapters | Interactive codex on one base branch | Shared code needs one owner and your attention |
| Migrate route groups | One git worktree add checkout per batch, one codex session each | Batches run in parallel without editing each other’s files |
| Contract: remove Express | codex cloud exec --env ENV_ID --branch BRANCH | A long, mechanical task that needs no laptop |
| First review of each batch | codex exec review --base BRANCH | A 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 requestset -euo pipefailcount=$( { 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 1fiThe 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:
git fetch origingit worktree add -b refactor/hono-auth ../api-hono-auth origin/maincd ../api-hono-auth && npm ci && codexcodex --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.
Step 5: Merge batches one at a time
Section titled “Step 5: Merge batches one at a time”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 filethis 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:
git fetch origingit push origin origin/main:refs/heads/refactor/hono-contractcodex cloud exec --env ENV_ID --branch refactor/hono-contract < docs/refactor/contract.mdcodex 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:
codex cloud diff TASK_URLgit fetch origingit worktree add -b refactor/hono-contract-apply ../api-hono-contract origin/refactor/hono-contractcd ../api-hono-contract && codex cloud apply TASK_URLnpm 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:
-
The oracle did not move.
git diff --stat origin/main...HEAD -- tests/http/is empty, or lists onlytests/http/setup.ts. -
The gates pass in CI, not only in the agent’s report:
npm run typecheck,npm run lintandnpm test. -
The ratchet fell.
scripts/refactor-ratchet.shreports fewer files, and.refactor-baselinewas lowered in the same pull request. -
The route inventory is identical. The diff against
docs/refactor/routes.txtis empty. -
An automated reviewer read the diff first. In the batch’s worktree, run:
Terminal window codex exec review --base origin/main--basecannot be combined with custom review instructions (checked in codex-cli 0.157.1: the command exits withthe argument '--base <BRANCH>' cannot be used with '[PROMPT]'). For a custom focus, run/reviewin the batch’s interactive session, or pipe the diff into a plaincodex execprompt, 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 reviewdoes the same. See Codex code review. -
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.
-
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.
When a large Codex refactor breaks
Section titled “When a large Codex refactor breaks”| Symptom | Cause | Recovery |
|---|---|---|
| Two batches conflict on the same file | The plan missed a shared import | Merge 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 changed | The agent weakened an assertion to match new behavior | The 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 merges | Each batch ran against a base without the other’s changes | Rebase open batches after every merge and rerun their gates, as in step 5 |
| Error responses change shape after migration | Hono’s default error handling differs from the Express error middleware | Add 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 function | The agent edited by hand, or matched on text instead of types | Revert, and rerun the codemod prompt: a type-aware script, dry-run count first |
The ratchet count goes up on main | A teammate’s feature branch added new Express routes | CI 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 app | The branch was not pushed, or --branch was omitted and the task ran on your current local branch | Push first and pass --branch explicitly |
| Commits vanished with a removed worktree | A managed worktree on a detached HEAD was removed before its commits reached a branch; its reflog went with it | Before 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 |
Where to go next with Codex refactoring
Section titled “Where to go next with Codex refactoring”The same workflow in other tools: refactoring with Claude Code and refactoring strategies in Cursor.