Claude Media
Claude Codeのxargs並列処理を安全に組む — find -print0と終了コードの集約

Claude Codeのxargs並列処理を安全に組む — find -print0と終了コードの集約

claude -pをxargs -Pで複数ファイルに並列実行するとき、空白入りファイル名・並列数・1件の失敗の拾い方の対処を、手元で動かした出力つきで示します。

claude -p を数十ファイルに回すなら、xargs -P で並列化するのが手軽です。ただし素の find | xargs は、空白を含むファイル名で壊れ、1件の失敗が全体の終了コードに残らず、並列数を上げすぎると費用と使用量だけが先に膨らみます。この記事は、その3点を手元で動かした出力つきで潰し、Claude Codeを呼ぶワーカーまで組みます。

公式の逐次ループを出発点にする

公式のベストプラクティスには、files.txt の各ファイルに claude -p を順番に回す例があります。

for file in $(cat files.txt); do
  claude --bare -p "Migrate $file to the new logger API and commit. Reply OK or FAIL." \
    --allowedTools "Read,Edit,Bash(git add *),Bash(git commit *)" \
    --permission-mode dontAsk \
    --max-turns 20
done

この例では、実行が終わるとClaudeが OK か FAIL を返し、ターン数の上限に達した実行はエラーで終了すると説明されています。さらに、まず3ファイルで試して失敗を見てから残りを回す進め方も添えられています。

この形には2つの弱点があります。逐次なので遅いこと、そして $(cat files.txt) が空白で単語分割するため、a b.txt が2つのファイルとして渡されることです。並列化はこの2点を同時に直す作業になります。

先に押さえる点

並列化で直す3つの穴

  • ファイル名の区切り

    改行や空白で区切ると、名前の途中で割れます。NUL区切りに切り替えます。

  • 失敗の集約

    xargs自身の終了コードだけでは、どのファイルが落ちたか分かりません。ワーカーで記録します。

  • 並列数

    増やすほど速くなる保証はなく、実行ごとにトークンが消費されます。小さく始めます。

空白・改行入りのファイル名は -print0と -0で渡す

手元のmacOS(標準のxargs)で、a b.txt・c.txt・名前の途中に改行を含む e\nf.txt を置いて試しました。find -print0 を使わず、改行区切りのまま xargs -n1 に渡すと、次のように割れます。

d/a
b.txt
d/c.txt
d/e
f.txt

a b.txt が d/a と b.txt に分かれています。-print0 と xargs -0 の組み合わせなら、名前が1個ずつ保たれます。

find d -type f -print0 | xargs -0 -n1 -P2 sh -c 'echo "got: $1"' _

出力は got: d/a b.txt のように、空白も改行も1つの引数のまま届きます。sh -c '…' _ の _ は $0 の埋め草で、ファイル名を $1 で受けるための定番の形です。

sh -c の中に {} を埋め込む書き方(-I{})は、ファイル名をシェルのコード文字列に埋め込むことになります。名前に引用符や $() が含まれると意図しない実行になるため、"$1" で受ける形のほうが安全です。

1件の失敗はxargsの終了コードだけでは追えない

次に、bad.txt のときだけ exit 1 するワーカーで試しました。

find d -type f -print0 | xargs -0 -n1 -P2 sh -c \
  'case "$1" in *bad*) exit 1;; esac; echo ok "$1"' _
echo "exit=$?"

他のファイルは ok を出して完走し、exit=1 が返りました。つまり失敗は検知できますが、どのファイルかは分かりません。

ワーカーが exit 255 を返すとさらに状況が変わります。手元のxargsは xargs: sh: exited with status 255; aborting と出して、残りの処理を打ち切りました。Claude Codeの呼び出しは1件ごとに時間も費用もかかるため、途中で止まるのか最後まで回るのかは、終了コードの値で変わります。ここはxargsの実装で細部が違うため、自分の環境で exit 1 と exit 255 の両方を一度試してから本番に使うと確実です。

終了コードに頼らず、次の方針にすると実装の差を気にしなくて済みます。

  • ワーカーは自分の失敗を failed.list に書き、自身は必ず0で返す
  • xargsが全件を回し終えたあと、failed.list が空かどうかで全体の終了コードを決める
  • ファイルごとのログを別名で残し、失敗の原因をあとから読めるようにする

Claude Codeを呼ぶワーカーを書く

ワーカーは、claude -p の結果が OK で始まるかと、プロセスの終了コードの両方を見ます。公式の例が、成功を OK、失敗を FAIL、上限到達をエラー終了で区別しているためです。あわせて --output-format json で受け取り、本文を .result、費用を .total_cost_usd から読みます。ヘッドレス実行のページには、json形式の応答に total_cost_usd とモデル別の内訳が入り、スクリプトから支出を追えると書かれています。.result の取り出しも同ページのjqの例と同じ書き方です。

#!/bin/sh
# worker.sh: 1ファイルを処理する。失敗は failed.list に追記し、自身は0で返す
f=$1
log="out/$(printf '%s' "$f" | tr '/ ' '__').log"
 
json=$(claude --bare -p "Migrate $f to the new logger API and commit. Reply OK or FAIL." \
  --output-format json \
  --allowedTools "Read,Edit,Bash(git add *),Bash(git commit *)" \
  --permission-mode dontAsk \
  --max-turns 20 --max-budget-usd 1.00 \
  </dev/null 2>"$log")
status=$?
 
printf '%s\n' "$json" >"$log.json"
result=$(printf '%s' "$json" | jq -r '.result // empty')
cost=$(printf '%s' "$json" | jq -r '.total_cost_usd // 0')
printf '%s\t%s\n' "$f" "$cost" >>out/cost.tsv
 
case "$status:$result" in
  0:OK*) echo "OK   $f" ;;
  *)     echo "$f" >>out/failed.list; echo "FAIL $f (log: $log)" ;;
esac
exit 0

成否は終了コードと .result の先頭で決め、費用は成功した件も失敗した件も out/cost.tsv に残します。jq が無い環境では入れておく必要があります。

--max-budget-usd はprintモード専用で、見積もり支出が上限に達すると実行を止めます(Claude Code v2.1.217以降は、上限到達後にサブエージェントの起動が Budget limit reached で失敗します)。ただし支出は上限を超えることがあるので、余裕を持たせた値にします。</dev/null は、標準入力を使わない呼び出しだと明示するための保険です。

呼び出し側は次のとおりです。

mkdir -p out && : > out/failed.list && : > out/cost.tsv
git ls-files -z -- 'src/*.ts' | xargs -0 -n1 -P3 ./worker.sh
 
awk -F'\t' '{s+=$2} END{printf "合計 %.2f USD(%d 件)\n", s, NR}' out/cost.tsv
 
if [ -s out/failed.list ]; then
  echo "失敗 $(wc -l < out/failed.list) 件" >&2
  cat out/failed.list >&2
  exit 1
fi

claude の代わりに、bad.txt の名前のときだけ非0で終わるダミーコマンドを置いて、このスクリプトを実際に動かしました。出力は次のとおりで、失敗が1件だけ拾われ、最後の判定で rc=1 になりました。

OK   d/a b.txt
FAIL d/bad.txt (log: out/d_bad.txt.log)
OK   d/c.txt
失敗        1 件
d/bad.txt

費用の合計は、失敗した件の分も cost.tsv に入っているので、止めた実行の支出も含めて数えられます。表示される額はクライアント側の見積もりで、実際の請求とずれることがあります。

集計で注意したいのは、--continue や --resume で続きの会話を回す場合です。その実行が報告する額は、会話全体の累計で、前の実行の支出も含まれます。ワーカーが毎回新しい会話を始める今回の形なら、1行が1件ぶんの額になり、足し合わせてそのまま合計になります。続きの会話を混ぜるときは、同じ会話の行を足し重ねないよう、最後の行だけを採ります。並列数を上げる前に、試運転の3件で cost.tsv の1件あたりの額を見ておくと、--max-budget-usd に置く値の見当がつきます。

失敗した件だけを再実行するときは、failed.list の改行をNULに直してxargsに戻します。ファイル名に改行を含む場合は、そもそも failed.list が1行1件で保てないため、その種のファイルがあるリポジトリでは記録をNUL区切りにします。

mv out/failed.list out/retry.list && : > out/failed.list
tr '\n' '\0' < out/retry.list | xargs -0 -n1 -P2 ./worker.sh

ファイルを渡す側は git ls-files -z のように、NUL区切りで出せるコマンドを選ぶと、改行入りの名前でも壊れません。

並列数は小さく始めて、使用量を見て上げる

-P に決まった正解はありません。手元は10コアですが、claude -p の待ち時間の大半はCPUでなくモデルの応答なので、コア数に合わせる理由はありません。数字を決める材料は次の3つです。

  • 同時に動く実行の数だけ、トークンの消費が重なる
  • 同じ作業ツリーを複数の実行が編集すると、ファイルの競合や git commit の取り合いが起きる
  • 1件あたりの上限(--max-turns、--max-budget-usd)は、並列数をかけた額が最悪値になる

-P0 は避けます。手元で xargs -0 -P0 -n1 echo を試すと、制限なしに同時起動する指定になり、数百件のファイルなら同数の claude が一斉に立ち上がります。

また、claude -p が5時間枠をどれだけ消費するかは、使い方で見え方が変わります。計測の切り分けはclaude -pが5時間枠を対話の約1.8倍消費する報告にまとめています。まず -P2 で3〜5件を回し、使用量と失敗率を見てから段階的に上げる進め方が無難です。

同一リポジトリの編集を並列にするなら、ファイルごとに別のworktreeを割り当てる手もあります。ベストプラクティスのページでは、claude -w feature-auth のように作業ツリーを分ける方法が並列作業の基本として挙げられています。xargsで回す場合は、ワーカーの中で git worktree add してから cd する形になります。次の例は、worker.sh の claude を呼ぶ部分だけをworktreeの中に移したものです。許可フラグと予算の上限は本体と同じものを残しています。dontAsk は事前に許可したツール以外を拒否するため、--allowedTools が欠けると編集もコミットも通りません。ログのリダイレクトは cd を含むサブシェルの外に置き、out/ を元のディレクトリから指します。ファイルごとにブランチを切るので、worktreeを消してもコミットはそのブランチに残ります。成果は後から git merge や cherry-pick で取り込めます。

dir=$(mktemp -d "${TMPDIR:-/tmp}/wt.XXXXXX")
branch="migrate/$(printf '%s' "$f" | tr '/ ' '__')"
git worktree add -q -b "$branch" "$dir" HEAD || { echo "$f" >>out/failed.list; exit 0; }
 
json=$( (cd "$dir" && claude --bare -p "Migrate $f ... Reply OK or FAIL." \
  --output-format json --permission-mode dontAsk --max-turns 20 \
  --allowedTools "Read,Edit,Bash(git add *),Bash(git commit *)" \
  --max-budget-usd 1.00 </dev/null) 2>"$log" )
status=$?
 
git worktree remove --force "$dir"

手元では claude をダミーに替えて、2件を -P2 で同時に回し、worktreeの作成から後片付けまでが衝突なく終わることを確かめました(確かめたのはこの範囲で、実際のモデル呼び出しは含みません)。worktreeは新しいcheckoutなので、.env のようなgitignore済みのファイルや依存関係は入っていません。必要なファイルは .worktreeinclude に書くと、Claude Codeが作るworktreeへコピーされます。git worktree add で自分で切った場合は対象外なので、ワーカーの中でコピーするか、依存のインストールを手順に入れます。また claude -p --worktree で作ったworktreeは終了時に確認なしで残るため、自分で git worktree remove して消します。並列実行の手段の選び方はClaude Codeの並列実行の使い分けが整理しています。

試運転と本番を分ける

3ファイルで試して、失敗を見てからプロンプトを直す手順は、並列でも同じです。

手順

本番前の試運転

  1. 1

    3ファイルで -P1

    ワーカーの判定(OK* の照合、failed.list への記録)が想定どおり動くかを見ます。

  2. 2

    わざと失敗を混ぜる

    --max-turns 1 にして落ちる件を作り、failed.list に入ることと、全体の終了コードが非0になることを確かめます。

  3. 3

    -P を2、3と上げる

    使用量とログの失敗率を見ながら上げます。失敗が増えたら戻します。

本番で一部が落ちたときは、前節の再投入の2行で failed.list の分だけを回し直します。再投入のたびに failed.list を空にしておけば、「空になるまで繰り返す」「N回で打ち切って人が見る」のどちらにも使えます。プロンプト側の問題で同じ件が何度も落ちるなら、再投入を重ねずにログの .result を読んで依頼文を直すほうが、費用の面でも早く片づきます。

CIに組み込む場合は、失敗した実行のログを成果物に残すと、原因の確認が早くなります。テスト失敗の修正を自動化する流れはClaude CodeでCIのテスト失敗を自動修正に書いています。

まとめ

xargsの並列化で外せないのは、NUL区切りの受け渡しと、失敗を自分で数えるワーカーの2つです。並列数と上限は、小さい値から実測で決めます。ログを claude -p に渡すときの10MB上限など、別の落とし穴はclaude -pにログを渡すと10MB上限で止まるが扱っています。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/best-practices
  2. code.claude.com/docs/en/headless
  3. code.claude.com/docs/en/cli-reference
  4. code.claude.com/docs/en/common-workflows
この記事を共有:XはてブLinkedIn