Claude Code のサブエージェントを本格的に使い始めると、必ずと言っていいほど遭遇するのが「いつまで経っても結果が返ってこない」「ログには何も出ていないのに処理が止まっている」という状況ではないでしょうか。最初は何が起きているのか見当もつかず、とりあえず Ctrl+C で中断するしかありません — 私もそうでした。
サブエージェントは便利な機能ですが、親エージェント側から見ると一種のブラックボックスになりがちです。ここでは、実際に何十回と詰まりながらたどり着いた「どこから調べるか」の判断フローを、症状ごとに整理して共有します。
症状で分類する — 最初の3つの質問
闇雲にログを眺めても時間を失うだけです。まずは次の3つを自問してください。
- サブエージェントは すぐに終わっている か、動きっぱなし か
- 出力はある か、何も出ていない か
- 親側に エラーが伝わっている か、サイレントに止まっている か
この3軸で症状を分けると、原因の候補が一気に絞れます。
- 即終了 × 出力なし × エラーあり → プロンプトや権限が不足している可能性
- 長時間稼働 × 出力なし × エラーなし → ツール呼び出しループや、コンテキスト逼迫
- 終了済み × 出力あり × エラーなし → プロンプト解釈のズレ
- 長時間稼働 × 出力あり × エラーなし → 親側でのストリーム処理の不備
ログを見る前にやること — 状態を保存する
診断に入る前に、現在のセッション状態を壊さないように保存します。復旧してから「再現しなくなった」では原因が追えません。
# セッションログを追記監視で開く(診断中も保存され続ける)
tail -f ~/.claude/logs/session-$(date +%Y%m%d)*.log > /tmp/cc-debug.log &
# 実行中のサブエージェント関連プロセスを確認
ps aux | grep -E "claude-(agent|code)" | grep -v grep
# 親エージェント側の現在の conversation を JSON に書き出す
claude --export-conversation /tmp/cc-conv.jsonこの状態を取った上で、はじめて /subagent stop <name> で停止してください。取らずに停止すると、なぜ止まっていたかの痕跡がまとめて消えます。
パターン1: 「動いているが何も出ていない」 — ツール呼び出しの無限ループ
経験上、最も多いのがこのパターンです。サブエージェントが同じツールを何度も呼び出して、親側から見ると「動いているけど進んでいない」ように見えます。
セッションログで次のように同じツール呼び出しが連続していたら確定です。
[subagent:tester] -> Bash: "npm test" (turn 7)
[subagent:tester] -> Bash: "npm test" (turn 8)
[subagent:tester] -> Bash: "npm test" (turn 9)原因は多くの場合、サブエージェントが 失敗の分析プロセスを持っていない ことです。テストが失敗したのに、原因を特定する前にもう一度同じテストを実行してしまう。対策はプロンプト側で「失敗時は原因を3つ列挙してから次の手を決める」と明示し、max_turns を 15 程度に制限することです。
---
# .claude/agents/tester.md
name: tester
tools: Bash, Read
max_turns: 15
---
あなたはテスト実行サブエージェントです。以下の手順を守ってください。
1. テストを1回だけ実行する
2. 失敗した場合、失敗したテスト名と原因の仮説を3つ列挙する
3. 検証可能な仮説を1つだけ選び、コードを読んで裏を取る
4. 修正すべき箇所が見つかったら「修正提案」セクションに記述する
5. 同じテストを再実行する前に、必ず「なぜ前回失敗したか」を書くmax_turns を指定しておけば、ループに入っても15ターンで強制終了します。ログに max_turns reached と出た場合、ツール呼び出しのループが起きていたことが確定します。
パターン2: 「いつの間にか終わっている」 — エラーが親に伝わらない
サブエージェント内で起きたエラーが、親エージェントから見ると「正常終了」に見えることがあります。たとえばファイル書き込みに失敗したのに、サブエージェントが最終出力として「完了しました」とだけ返してしまうケースです。これは文字通りサイレント障害で、気づくのが翌日ということもあります。
対策は、サブエージェント側で 構造化された終了レポート を必須にすることです。親のプロンプトに次のような制約を書いておきます。
{
"status": "success | partial | failed",
"files_modified": ["src/utils/retry.ts"],
"tests_passed": true,
"unresolved_issues": [],
"next_action_recommended": "..."
}「最終出力は必ずこのJSON形式で、どれか1つでも欠けたら失敗扱いにする」と明示すれば、曖昧な「完了しました」を信じる必要がなくなります。失敗した作業があった場合は status を "partial" または "failed" とし、unresolved_issues に具体的な問題を列挙させます。
親側では status を機械的に判定できるため、オーケストレーション全体の信頼性が一段上がります。
パターン3: 「結果は返るが内容がズレている」 — 出力スキーマの不在
サブエージェントが何かを返すものの、親が期待する形ではない場合、原因はほぼ確実にプロンプトが「何を返すか」を指定していないことです。
対策は2つあります。
- サンプル出力を1つ以上示す。説明ではなく実例で示した方が、解釈のブレが劇的に減ります
- 親側でバリデーションを走らせる。期待したスキーマに合わなければ、1度だけリトライする設計にします
// 親エージェント側の擬似コード
const result = await runSubagent('refactor-agent', task);
const parsed = tryParseJSON(result);
if (!parsed || !parsed.status) {
// スキーマ違反 — 欠けているフィールドを具体的に伝えてリトライ
return await runSubagent('refactor-agent', {
...task,
retry_reason: 'status フィールドが欠けています。必ず前回示したJSON形式で返してください',
});
}
// リトライは1回だけ。それ以上は設計を疑う段階
return parsed;リトライを無制限にすると、今度は無限ループの原因になります。1回に制限するのがコツです。
それでも動かないとき — .claude/agents/ のリロード
設定ファイルを変更したのに挙動が変わらない場合、サブエージェント定義がキャッシュされている可能性があります。
# agent 定義のキャッシュをクリア(開発中は毎回やって損はない)
rm -rf ~/.claude/cache/agents/
# 念のためセッションを新規作成する
claude --new-sessionより踏み込んだ制御を設計するなら、Claude Code サブエージェントのオーケストレーション で複数エージェント構成時の役割分担を扱っています。長時間セッションでコンテキストが壊れるケースは 長時間セッションのコンテキスト保全 と、ツール連鎖が詰まるケースは Claude Code Hooks の本番デバッグ手法 と組み合わせて読むと、症状から原因への道筋が短くなります。
個人開発で Dolice Labs の自動投稿をサブエージェントに任せていたとき、私自身いちばん消耗したのが、この「返ってこない」沈黙でした。エラーが出れば調べようがありますが、無言で止まると原因の当たりすらつきません。max_turns と終了レポートの構造化を入れてからは、少なくとも「どこで止まったか」が必ず残るようになりました。
次に試すこと
今日の作業で詰まっているサブエージェントがあれば、まずは max_turns を定義ファイルに追加してください。これだけでループ起因の「戻ってこない」症状の大半は可視化できます。
そこまで終わったら、次は終了レポートをJSON構造化するところまで進めてみてください。出力のフォーマットが固定化されるだけで、「何が起きているか分からない」状況そのものが起きにくくなります。