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, aclaude -pscript 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
/batchprompt 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.
Which batch approach fits the change?
Section titled “Which batch approach fits the change?”Pick by how independent the pieces are, not by file count alone.
| Approach | Use it when | What you get back | Where verification runs |
|---|---|---|---|
| One session, plan then execute | Fewer than about 20 files, or edits that must land in dependency order | One branch, reviewed batch by batch | Tests you ask Claude to run after each batch |
/batch | The change splits into independent units, each mergeable on its own | 5 to 30 pull requests, one per unit | Inside every worker, then in CI on each PR |
claude -p loop | You rerun the same change per directory, per package or per repository, or from CI | JSON results per run, commits you make | In your script, outside the agent |
| Dynamic workflow | Audits or changes that need dozens to hundreds of agents and a rerunnable script | A saved workflow and its report | In the workflow’s own verification phase |
| A codemod the agent writes | The transform is purely syntactic (an import path, a renamed symbol) | A deterministic script you can rerun | In 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.
-
Inventory every call site. Paste the scoping prompt below. Claude searches the codebase and returns one row per file.
-
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.
-
Land shared prerequisites first. Add
date-fnstopackage.json, create any shared helper such assrc/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. -
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.
-
Choose the execution approach from the table above and hand over the approved table as the plan.
Run a large change with /batch
Section titled “Run a large change with /batch”/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.
- 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.
- Spawn workers. One background agent per unit, each in an isolated git worktree. Every worker implements its unit, runs the
/code-reviewskill and fixes what it finds, runs the test suite and the end-to-end recipe, then commits, pushes and opens a pull request withgh pr create. - 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.
Script repeatable batches with claude -p
Section titled “Script repeatable batches with claude -p”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 branchset -euo pipefailresults=$(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"doneecho "Run logs: $results"What each flag does, as listed by claude --help in v2.1.283:
--permission-mode acceptEditslets 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.--allowedToolspre-approves only the type check and the test runner.--disallowedTools "Edit(**/*.test.ts)"keeps the oracle out of reach.--permission-prompts nonedenies anything that would otherwise prompt, instead of waiting.--max-budget-usd 5stops a runaway run. The JSON result then carries the subtypeerror_max_budget_usdinstead ofsuccess, which thejqcheck 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:
| Change | Completeness check |
|---|---|
| Library migration | git grep for the old import prints nothing |
| Rename a class or module | git grep -w OldName prints nothing, and npx tsc --noEmit passes |
| Move a directory | Every import resolves (npx tsc --noEmit), and the old path appears in no config file |
| Remove an API version | Every removed route had a successor, and requests to the old routes fail in integration tests |
| Add boilerplate to every handler | A 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.
- Scope: every changed file sits inside the directories the plan named.
- Completeness: the old pattern no longer appears anywhere.
- Oracle intact: no test file changed in the same unit that changed the code.
- Behaviour: the type check, the unit tests and the characterization tests pass, and CI runs the full suite on each pull request.
- 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 branchset -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 changedpattern="^($(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 unitif git grep -nE "from ['\"]moment['\"]|require\(['\"]moment['\"]\)" -- "${UNIT_DIRS[@]}"; then echo "moment is still imported in ${UNIT_DIRS[*]}"; exit 1fi
# 3. Oracle intact: no test or snapshot changedif echo "$changed" | grep -E '\.test\.ts$|__snapshots__/'; then echo "Test files changed: review these by hand before merging"; exit 1fi
# 4. Behaviour: the whole suite, not only this unit's testsnpx tsc --noEmitnpx jest --ciThe 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.
Tests pass because the agent changed them
Section titled “Tests pass because the agent changed them”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.
Edits land outside the planned scope
Section titled “Edits land outside the planned scope”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.
A headless run stops on its budget
Section titled “A headless run stops on its budget”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.
Where to go next with batch operations
Section titled “Where to go next with batch operations”- Multi-file workflows cover coordinated changes whose files depend on each other, the case
/batchis not built for. - Database and code migrations apply the same plan-first approach to schema and framework upgrades.
- Dynamic workflows take over when a job needs dozens to hundreds of agents and a script you rerun.
- Agent view shows the background agents from one screen while
/batchruns. - Review automation puts an automated reviewer on each unit’s pull request.
- Bulk changes in Codex is the sibling page for Codex cloud tasks and worktrees.