How Do I Run Parallel Batch Operations with Claude Code CLI?
I had a codebase with 50 React class components that needed migration to hooks. Running Claude Code sequentially meant 30-120 seconds per file — that’s potentially 100 minutes of waiting. There had to be a better way.
The Problem
I started with a simple loop:
for file in $(cat files-to-migrate.txt); do claude "Migrate $file from class components to hooks"doneThis worked, but it was painfully slow. Each file took around a minute, and I was watching my terminal like it was a loading screen from the 90s. My CPU sat mostly idle while waiting for API responses.
Discovery: The -p Flag
I dug into Claude Code’s CLI options and found the -p flag. It enables non-interactive, prompt-driven execution:
claude -p "Migrate src/components/App.js from class components to hooks"This runs without any user interaction. But it still processes one file at a time.
Parallel Processing with Shell Backgrounding
The solution was combining -p with shell backgrounding (&) and wait:
for file in $(cat files-to-migrate.txt); do claude -p "Migrate $file from class components to hooks" &done
waitecho "All migrations complete"The & runs each Claude instance in the background, and wait blocks until all background jobs finish. My 50-file migration went from 100 minutes to about 15 minutes.
Safety with —allowedTools
I got nervous about giving Claude full autonomy across 50 parallel instances. What if one decides to refactor something unexpected?
The --allowedTools flag solves this:
for file in $(cat files-to-migrate.txt); do claude -p "Migrate $file from class components to hooks" \ --allowedTools "Edit,Bash(git commit *)" &done
waitNow each Claude instance can only use Edit and run git commits. No Write (which overwrites files), no Bash(rm *), no web fetching.
Tool Scoping Options
# Read-only analysis--allowedTools "Read,Grep,Glob"
# File editing only (no shell commands)--allowedTools "Edit,Write"
# Git operations only--allowedTools "Bash(git *)"
# Full editing with git commits--allowedTools "Edit,Write,Bash(git add *),Bash(git commit *)"
# Web fetching (no file modifications)--allowedTools "WebFetch"Controlling Concurrency
My first parallel run spawned 50 Claude instances simultaneously. API rate limits laughed in my face.
I needed to limit concurrency:
#!/bin/bashMAX_PARALLEL=5FILES="files-to-migrate.txt"count=0
for file in $(cat "$FILES"); do claude -p "Migrate $file from class components to hooks" \ --allowedTools "Edit,Bash(git commit *)" &
count=$((count + 1))
if [ $count -ge $MAX_PARALLEL ]; then wait -n # Wait for any one job to finish count=$((count - 1)) fidone
waitecho "All migrations complete"The wait -n waits for any single background job to finish, keeping at most 5 running at once.
Adding Logging and Error Handling
Parallel execution makes it hard to know what succeeded or failed. I added logging:
#!/bin/bashLOG_DIR="./migration-logs"mkdir -p "$LOG_DIR"
for file in $(cat files-to-migrate.txt); do filename=$(basename "$file") ( claude -p "Migrate $file from class components to hooks" \ --allowedTools "Edit,Bash(git add $file),Bash(git commit -m 'Migrate $filename to hooks')" \ > "$LOG_DIR/${filename}.log" 2>&1
if [ $? -eq 0 ]; then echo "SUCCESS: $file" >> "$LOG_DIR/summary.log" else echo "FAILED: $file" >> "$LOG_DIR/summary.log" fi ) &done
waitecho "Migration complete. Check $LOG_DIR/summary.log for results."Each file gets its own log file, and a summary tracks overall success/failure.
Real-World Use Cases
CommonJS to ESM Conversion
for file in $(find ./src -name "*.js"); do claude -p "Convert $file from CommonJS (require/module.exports) to ESM (import/export). Keep all functionality identical." \ --allowedTools "Edit" &donewaitImport Path Updates
OLD_PKG="@company/old-lib"NEW_PKG="@company/new-lib"
for file in $(grep -rl "$OLD_PKG" ./src); do claude -p "In $file, replace all imports from '$OLD_PKG' with '$NEW_PKG'. Keep all other imports unchanged." \ --allowedTools "Edit" &donewaitTypeScript File Extension Migration
for file in $(find ./src -name "*.ts" -not -name "*.tsx"); do newfile="${file%.ts}.tsx"
claude -p "Convert $file to TypeScript JSX. Read the file, add React JSX support, then write to $newfile" \ --allowedTools "Read,Write,Bash(mv $file $newfile)" &donewaitVisualizing the Speedup
Sequential (50 files, 60s each):File 1 [========================================] 60sFile 2 [========================================] 60sFile 3 [========================================] 60s...File 50 [========================================] 60sTotal: ~50 minutes
Parallel (5 concurrent, 50 files):File 1-5 [========================================] 60s (concurrent)File 6-10 [========================================] 60s (concurrent)File 11-15 [========================================] 60s (concurrent)...File 46-50 [========================================] 60s (concurrent)Total: ~10 minutesCommon Mistakes
- Over-parallelizing: 50+ parallel instances hit API limits fast. Keep it to 5-10.
- Broad tool scoping:
--allowedTools "*"is asking for trouble. - Missing
wait: Script exits before Claude finishes. - Dependent files: If file B depends on file A’s changes, parallel fails.
- No error handling: Silent failures are impossible to debug.
Using xargs as an Alternative
For those who prefer xargs:
cat files-to-migrate.txt | xargs -P 5 -I {} claude -p \ "Migrate {} from class components to hooks" \ --allowedTools "Edit,Bash(git commit *)"The -P 5 limits to 5 parallel processes.
What I Learned
The -p flag transforms Claude Code from an interactive assistant into a batch processing engine. Combined with --allowedTools for safety and shell backgrounding for parallelism, it becomes a powerful automation tool.
My migration workflow now looks like:
- Test the prompt on 2-3 files sequentially
- Verify the output quality
- Scope tools to minimum necessary
- Run parallel with 5 concurrent instances
- Review logs and summary
The 50-file migration that would have taken 100 minutes now completes in under 15 minutes, and I can move on to other work while it runs.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments