Skip to content

Batch operations: one change across hundreds of files

Batch operations in Claude Code apply one change, such as a library migration, a rename or an API version removal, across dozens or hundreds of files. The bundled /batch skill plans the change, splits it into 5 to 30 independent units and runs each unit in its own git worktree as a background agent that opens a pull request.

This page is for developers who own a sweeping change and for tech leads who have to merge the result. The situation: your team is moving from moment.js to date-fns, 147 files import moment, and each uses a different slice of its API. Find-and-replace fails because moment().format('YYYY-MM-DD') becomes format(new Date(), 'yyyy-MM-dd'), and nobody wants to review 147 diffs line by line.

Everything below was checked against Claude Code v2.1.283 (the latest channel) on 2026-09-26.

What you get from a batch-operation workflow

Section titled “What you get from a batch-operation workflow”
  • A decision table for choosing between one session, /batch, a claude -p script and a dynamic workflow before you spend tokens.
  • A prompt that has Claude write a codemod you review in 30 lines instead of 300 edited files.
  • A plan-first prompt that produces a file-by-file migration table you approve before anything changes.
  • A /batch prompt that carries its own verification recipe into every worker.
  • A headless loop with a spending cap, a narrow tool allow list and tests the agent cannot edit.
  • A verification script that proves scope, completeness and behaviour without reading every diff.
  • Recovery steps for merge conflicts between units, weakened tests, budget stops and runs that never open a PR.

Pick by how independent the pieces are, not by file count alone.

ApproachUse it whenWhat you get backWhere verification runs
One session, plan then executeFewer than about 20 files, or edits that must land in dependency orderOne branch, reviewed batch by batchTests you ask Claude to run after each batch
/batchThe change splits into independent units, each mergeable on its own5 to 30 pull requests, one per unitInside every worker, then in CI on each PR
claude -p loopYou rerun the same change per directory, per package or per repository, or from CIJSON results per run, commits you makeIn your script, outside the agent
Dynamic workflowAudits or changes that need dozens to hundreds of agents and a rerunnable scriptA saved workflow and its reportIn the workflow’s own verification phase
A codemod the agent writesThe transform is purely syntactic (an import path, a renamed symbol)A deterministic script you can rerunIn the codemod’s dry run plus your test suite

The last row is the one teams skip. When every call site changes the same way, ask Claude to write an ast-grep rule or a jscodeshift transform, and review 30 lines of transform instead of 300 edited files. Both run through npx without a project install: npx -p @ast-grep/cli ast-grep and npx jscodeshift, which has a --dry flag. The ast-grep package ships binaries named ast-grep and sg, so a bare npx @ast-grep/cli fails with “could not determine executable to run” (checked with @ast-grep/cli 0.45.3 and jscodeshift 17.4.0 on 2026-09-26). Reserve agent edits for the call sites the codemod cannot express.

Plan the change before any file is touched

Section titled “Plan the change before any file is touched”

Every approach starts the same way: a complete inventory that you approve. Start the session in plan mode (/plan, or Shift+Tab until the mode indicator reads plan), so Claude can read and search but cannot edit.

  1. Inventory every call site. Paste the scoping prompt below. Claude searches the codebase and returns one row per file.

  2. Read the flagged rows, not every row. The rows marked NO DIRECT EQUIVALENT are where the migration can change behaviour. Decide each one yourself: keep a shim, rewrite by hand, or accept a behaviour change and write it down.

  3. Land shared prerequisites first. Add date-fns to package.json, create any shared helper such as src/lib/dates.ts, and merge that as its own small pull request. If you skip this, every unit adds the dependency separately and the pull requests conflict on the lockfile.

  4. Pin current behaviour. For code with thin coverage, add characterization tests that record today’s output before any unit starts. These tests are the oracle for the whole migration.

  5. Choose the execution approach from the table above and hand over the approved table as the plan.

/batch is a bundled skill that only you can start: Claude does not invoke it on its own (checked in v2.1.283). Type /batch followed by the instruction. The skill then works in three phases.

  1. Research and plan, in plan mode. Research subagents find every file and call site. Claude splits the work into 5 to 30 self-contained units, preferring one unit per directory or module, and writes an end-to-end test recipe that each worker can run on its own. If it cannot find one, it asks you how to verify the change. You approve the plan before anything runs.
  2. Spawn workers. One background agent per unit, each in an isolated git worktree. Every worker implements its unit, runs the /code-review skill and fixes what it finds, runs the test suite and the end-to-end recipe, then commits, pushes and opens a pull request with gh pr create.
  3. Track progress. Claude keeps a status table with each unit’s state and pull request link, and ends with a one-line summary of how many units landed as pull requests.

Before you run it, check three prerequisites. The directory must be a git repository (worktree mode fails without one unless you configure WorktreeCreate hooks). The GitHub CLI must be installed and authenticated with push rights; without it, workers finish but report no pull request. Your test command must be discoverable, ideally written in CLAUDE.md, because every worker looks for it.

The last rule matters most. The bundled worker instructions tell each worker to fix failing tests (checked in v2.1.283), so without this line a worker may rewrite the expectation. A worker that can edit the test it is failing will eventually make it pass that way. The prompt forbids it, and the verification script below detects it. For a mechanical block, add an Edit deny rule for test files to .claude/settings.json, as protecting the oracle describes.

For a tech lead, /batch turns one change into up to 30 pull requests at once. Agree before the run who merges them and in what order, and make sure CI runs the full suite on each one. The review queue guide covers sizing that load.

A print-mode loop fits when you repeat the same change across packages or repositories, or when it must run in CI. The script, not the agent, runs the decisive checks, and it stops at the first failure.

#!/usr/bin/env bash
# migrate-moment.sh: run in the terminal from the repo root, on a fresh branch
set -euo pipefail
results=$(mktemp -d) # keep run logs out of the repo, or git add would commit them
for dir in src/utils src/components src/api src/services; do
out="$results/$(echo "$dir" | tr / _).json"
claude -p "Migrate every moment import in $dir/ to date-fns, following the \
approved plan in docs/migration/moment-to-date-fns.md. Touch only files under $dir/. \
When done, run npx tsc --noEmit and npx jest $dir --ci and report the results." \
--permission-mode acceptEdits \
--allowedTools "Bash(npx tsc *)" "Bash(npx jest *)" \
--disallowedTools "Edit(**/*.test.ts)" \
--permission-prompts none \
--max-budget-usd 5 \
--output-format json > "$out" || true
status=$(jq -r '.subtype // "missing"' "$out")
if [ "$status" != "success" ]; then
echo "Stopped at $dir: $status"; exit 1
fi
# The script, not the agent, decides whether the batch is good.
if git status --porcelain | grep -Ev " $dir/"; then
echo "Changes outside $dir"; exit 1
fi
if git status --porcelain -- "$dir" | grep -E '\.test\.ts$|__snapshots__/'; then
echo "Test files changed in $dir"; exit 1
fi
npx tsc --noEmit
npx jest "$dir" --ci
git add -- "$dir" && git commit -m "refactor($dir): migrate moment to date-fns"
done
echo "Run logs: $results"

What each flag does, as listed by claude --help in v2.1.283:

  • --permission-mode acceptEdits lets Claude edit files without asking. Print mode does not start in auto mode, so without it every edit would need an approval that nobody can give.
  • --allowedTools pre-approves only the type check and the test runner. --disallowedTools "Edit(**/*.test.ts)" keeps the oracle out of reach.
  • --permission-prompts none denies anything that would otherwise prompt, instead of waiting.
  • --max-budget-usd 5 stops a runaway run. The JSON result then carries the subtype error_max_budget_usd instead of success, which the jq check catches.

The deny rule covers Claude’s file-editing tools, not every way a process can write a file, so the script checks the working tree itself: any change outside $dir, or to a test or snapshot, stops the loop before the commit.

Common batch operations and the prompts for them

Section titled “Common batch operations and the prompts for them”

The same pattern covers most sweeping changes. What changes is the completeness check, the command that proves nothing was missed:

ChangeCompleteness check
Library migrationgit grep for the old import prints nothing
Rename a class or modulegit grep -w OldName prints nothing, and npx tsc --noEmit passes
Move a directoryEvery import resolves (npx tsc --noEmit), and the old path appears in no config file
Remove an API versionEvery removed route had a successor, and requests to the old routes fail in integration tests
Add boilerplate to every handlerA lint rule or git grep -L lists no handler without it

How do you verify a batch change without reading every diff?

Section titled “How do you verify a batch change without reading every diff?”

A batch of 150 files is past the point where line-by-line review is honest. Replace reading with checks that fail loudly, and read only where they point.

  1. Scope: every changed file sits inside the directories the plan named.
  2. Completeness: the old pattern no longer appears anywhere.
  3. Oracle intact: no test file changed in the same unit that changed the code.
  4. Behaviour: the type check, the unit tests and the characterization tests pass, and CI runs the full suite on each pull request.
  5. Targeted reading: you read only the NO DIRECT EQUIVALENT call sites and any file a check flagged.

Run this script on each unit branch, locally or as a CI job, with the unit’s directories as arguments (./verify-batch.sh src/utils, or ./verify-batch.sh src/utils src/reports for a unit that spans two):

#!/usr/bin/env bash
# verify-batch.sh DIR [DIR...]: run from the repo root on the unit's branch
set -euo pipefail
[ $# -gt 0 ] || { echo "usage: verify-batch.sh src/utils [src/other ...]"; exit 2; }
UNIT_DIRS=("$@")
BASE=${BASE:-origin/main}
changed=$(git diff --name-only "$BASE"...HEAD)
# 1. Scope: nothing outside this unit's directories changed
pattern="^($(IFS='|'; echo "${UNIT_DIRS[*]}"))/"
outside=$(echo "$changed" | grep -vE "$pattern" || true)
[ -z "$outside" ] || { echo "Out of scope:"; echo "$outside"; exit 1; }
# 2. Completeness: no moment import is left in this unit
if git grep -nE "from ['\"]moment['\"]|require\(['\"]moment['\"]\)" -- "${UNIT_DIRS[@]}"; then
echo "moment is still imported in ${UNIT_DIRS[*]}"; exit 1
fi
# 3. Oracle intact: no test or snapshot changed
if echo "$changed" | grep -E '\.test\.ts$|__snapshots__/'; then
echo "Test files changed: review these by hand before merging"; exit 1
fi
# 4. Behaviour: the whole suite, not only this unit's tests
npx tsc --noEmit
npx jest --ci

The completeness check is scoped to the unit on purpose. On a unit branch the other directories still import moment, so a repository-wide git grep would fail every unit. Run the repository-wide version once on main after the last unit merges: git grep -nE "from ['\"]moment['\"]" -- src printing nothing is the check that says the migration is finished, and that is when you remove moment from package.json. The units that hold NO DIRECT EQUIVALENT call sites can span directories: pass every directory the plan lists for that unit, and read those diffs anyway.

Sign-off splits cleanly. The developer who ran the batch approves the plan table and the flagged rows. CI owns the green checks on every unit. The reviewer of each pull request reads the flagged call sites and the check output, not the whole diff. To make the type check and the suite stronger gates, see testing with Claude Code and architecture fitness functions.

What breaks in a batch operation, and how do you recover?

Section titled “What breaks in a batch operation, and how do you recover?”

Units conflict when their pull requests merge

Section titled “Units conflict when their pull requests merge”

Symptom: the first few /batch pull requests merge, and the rest conflict on a barrel file, the router config or the lockfile. Cause: the units were not independent, because a shared file was part of several of them. Recovery: close the conflicting pull requests, move the shared file into a separate first pull request, merge it, and rerun /batch from the updated main branch for the remaining directories. Prevent it with step 5 of the scoping prompt.

A worker finishes but opens no pull request

Section titled “A worker finishes but opens no pull request”

Symptom: the status table shows a unit as done with no link, and the worker’s result reads PR: none with a reason. Cause: usually the GitHub CLI is missing or unauthenticated, or the push was rejected. Recovery: fix gh auth status, then push the branch from the worker’s worktree and open the pull request by hand. The code is already committed there.

Symptom: a green unit whose diff touches *.test.ts or snapshot files. Cause: the worker met a failing test and “fixed” the expectation. Recovery: revert the test changes, rerun the tests, and treat each failure as a real behaviour difference to decide. Prevent it with the Edit deny rule and check 3 of the verification script.

Symptom: the scope check lists files in src/core/ or in config. Cause: the agent followed a type error outward. Recovery: revert those files, then add the dependency to the plan as its own unit or prerequisite. Out-of-scope edits usually mean the plan missed a shared file.

The context fills up during a long single session

Section titled “The context fills up during a long single session”

Symptom: deep into a long session covering dozens of files, answers get vaguer and earlier decisions are forgotten. Recovery: commit what passes, /clear, and restart from the plan table with the finished directories marked done. For more than about 20 files, move to /batch or custom subagents, which each get their own context window.

Symptom: the loop exits with error_max_budget_usd. Cause: the directory was bigger or harder than the others. Recovery: split that directory in the loop’s list and rerun from it; the directories already committed stay done. To track what the runs cost, see cost control.

Claude keeps fixing one file and breaking another

Section titled “Claude keeps fixing one file and breaking another”

Symptom: the same two or three files alternate between failing and passing. Cause: a dependency order the plan ignored. Recovery: stop the run, ask for the dependency order in plan mode, and execute those files as one serial batch before fanning out the rest.