Skip to content

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:

naive-sequential.sh
for file in $(cat files-to-migrate.txt); do
claude "Migrate $file from class components to hooks"
done

This 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:

single-file-prompt.sh
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:

parallel-basic.sh
for file in $(cat files-to-migrate.txt); do
claude -p "Migrate $file from class components to hooks" &
done
wait
echo "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:

parallel-scoped.sh
for file in $(cat files-to-migrate.txt); do
claude -p "Migrate $file from class components to hooks" \
--allowedTools "Edit,Bash(git commit *)" &
done
wait

Now 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

allowedtools-examples.txt
# 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:

parallel-limited.sh
#!/bin/bash
MAX_PARALLEL=5
FILES="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))
fi
done
wait
echo "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:

parallel-with-logging.sh
#!/bin/bash
LOG_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
wait
echo "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

cjs-to-esm.sh
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" &
done
wait

Import Path Updates

update-imports.sh
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" &
done
wait

TypeScript File Extension Migration

ts-to-tsx.sh
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)" &
done
wait

Visualizing the Speedup

timing-comparison.txt
Sequential (50 files, 60s each):
File 1 [========================================] 60s
File 2 [========================================] 60s
File 3 [========================================] 60s
...
File 50 [========================================] 60s
Total: ~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 minutes

Common Mistakes

  1. Over-parallelizing: 50+ parallel instances hit API limits fast. Keep it to 5-10.
  2. Broad tool scoping: --allowedTools "*" is asking for trouble.
  3. Missing wait: Script exits before Claude finishes.
  4. Dependent files: If file B depends on file A’s changes, parallel fails.
  5. No error handling: Silent failures are impossible to debug.

Using xargs as an Alternative

For those who prefer xargs:

xargs-parallel.sh
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:

  1. Test the prompt on 2-3 files sequentially
  2. Verify the output quality
  3. Scope tools to minimum necessary
  4. Run parallel with 5 concurrent instances
  5. 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