はじめに
claude -p ... --output-format stream-json の出力を jq で1行だけ拾って自動化を組んでいる人は多いはずです。「最初に来た result タイプの行が最終結果」という前提は、サブエージェントが絡まない限り正しく動きます。
ところが Claude Code 2.1.211(Week 29・2026-07-13〜17の更新)で追加された --forward-subagent-text を使い、実際にバックグラウンドのサブエージェントを1つ起動しただけの単純なリクエストを実行してみると、1回の claude -p 呼び出しの中で result タイプの行が 2回 出てきました。しかも最初の result は「まだ実行中です」という中間報告で、本当に欲しい最終結果は2番目の result にしか入っていません。
jq -r '.result' を素朴にパイプする自動化は、先頭の result を拾って処理を終えてしまう可能性があります。この記事では実際に流したコマンドとその生ログを示しながら、何が起きているのかを公式ドキュメントの記述と突き合わせて確認します。
この記事で学べること
-
--forward-subagent-text/CLAUDE_CODE_FORWARD_SUBAGENT_TEXTが stream-json 出力に何を追加するか -
parent_tool_use_idを使ってメインエージェントとサブエージェントの発言を区別する方法 - 非同期(バックグラウンド)サブエージェントを使うと
resultイベントが複数回出るケースがあること -
claude -pを自動化パイプラインに組み込むときに気をつけるべき点
対象読者
-
claude -pをCI・スクリプト・自前のオーケストレーションに組み込んでいる方 - Claude Agent SDK(CLI版)のstream-json出力をパースするコードを書く方
- サブエージェントの動きをログとして可視化したい方
前提環境
- Claude Code CLI 2.1.211(
claude --versionで確認) - 実行場所: Claude Code on the web のクラウド実行コンテナ(Linux)
- 検証は
--tools "Task"--permission-mode dontAskで限定した非対話セッションを、リポジトリ外の一時ディレクトリで実行(本番の作業ツリーには影響しない)
TL;DR
-
--forward-subagent-text(v2.1.211〜)を付けると、サブエージェントのthinking/textブロックもassistantメッセージとして stream-json に流れてくる(デフォルトはtool_use/tool_resultのみ) - サブエージェントのメッセージは
parent_tool_use_idに「そのサブエージェントを起動したtool_useのID」が入る。メインの発言はparent_tool_use_id: null - サブエージェントを バックグラウンド(非同期) で起動すると、
claude -pは1回の呼び出し中でresultタイプの行を複数回出すことがある。最初のresultは暫定の応答で、本当の最終出力は末尾のresultにしか入っていない
検証: 実際に動かしてみる
ステップ1: 何もない状態のstream-json
まずサブエージェントを使わない単純なリクエストで、ベースラインを確認します。
claude -p "Say hello in one short sentence." \
--output-format=stream-json --model haiku --tools "" \
--no-session-persistence --safe-mode --verbose
このケースでは system(init) → assistant(thinking) → assistant(text) → result の順で4行出力され、result タイプの行は1回だけでした。サブエージェントが登場しない限り「result は1回」という前提は成り立ちます。
ステップ2: --forward-subagent-text でサブエージェントを覗く
次に、Task(Agent)ツールだけを許可し、サブエージェントを1つ起動させるプロンプトを与えます。
claude -p "Use the Task/Agent tool once to launch a general-purpose subagent \
with the exact instruction 'Reply with only the word DONE, no explanation.' \
Do not ask for confirmation. Do nothing else." \
--output-format=stream-json --forward-subagent-text --model haiku \
--tools "Task" --permission-mode dontAsk --max-budget-usd 0.10 \
--no-session-persistence --safe-mode --verbose
出力は48行に増えました。メインエージェントがサブエージェントを起動する行はこうなります(agentId は内部識別子のため伏せています)。
{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_01ChSAq3quLKn5dAkuCMeP4p","name":"Agent","input":{"description":"Simple agent test","subagent_type":"general-purpose","prompt":"Reply with only the word DONE, no explanation."}}]},"parent_tool_use_id":null, ...}
続くツール結果は「非同期でエージェントを起動した」という内容で、system イベントとして background_tasks_changed と task_started も流れてきます。
{"type":"system","subtype":"background_tasks_changed","tasks":[{"task_id":"<redacted>","task_type":"local_agent","description":"Simple agent test"}]}
{"type":"system","subtype":"task_started","task_id":"<redacted>","tool_use_id":"toolu_01ChSAq3quLKn5dAkuCMeP4p","subagent_type":"general-purpose","task_type":"local_agent","prompt":"Reply with only the word DONE, no explanation."}
ここまでは通常の非同期エージェント管理イベントです。--forward-subagent-text の効果が出るのはこの直後で、サブエージェント自身の思考・返答が assistant メッセージとして流れてきます。
{"type":"assistant","message":{"content":[{"type":"thinking","thinking":"The user is asking me to reply with only the word \"DONE\"..."}]},"parent_tool_use_id":"toolu_01ChSAq3quLKn5dAkuCMeP4p"}
{"type":"assistant","message":{"content":[{"type":"text","text":"DONE"}]},"parent_tool_use_id":"toolu_01ChSAq3quLKn5dAkuCMeP4p"}
この2行の parent_tool_use_id は、サブエージェントを起動した tool_use のIDと一致しています。--forward-subagent-text を付けなければ、この2行は流れず、サブエージェントの完了はツール結果(tool_result)としてしか分かりません。
観察: result タイプの行が2回出た
全48行を type で見ていくと、result タイプの行は次の2つだけでした。
| 出現位置 | subtype |
result の中身 |
num_turns |
duration_ms |
|---|---|---|---|---|
| 35行目 | success | Agent launched. Awaiting response. |
2 | 5523 |
| 48行目(最終行) | success | DONE |
1 | 2107 |
つまりメインエージェントは「起動しました。応答を待っています」という発言を自分の1ターン目の結論として result に出力し、そこで見かけ上いったん完結します。ところがサブエージェントの完了通知が届くと、claude -p のプロセスはまだ生きたままもう1ターン実行し、最終的な DONE を含む2つ目の result を出してから終了しました。
なぜこうなるのか
Anthropicのheadless実行ドキュメントには次の記述があります。
Background subagents and workflows are exempt from the five-second grace because their result is part of the final output, so
claude -pwaits for them to complete. From v2.1.182, that wait is capped at ten minutes by default so a stuck background agent cannot hold the process open indefinitely.
— Run Claude Code programmatically(Background tasks at exit)
「サブエージェントの完了を待ってからプロセスを終了する」という説明は正しく、実際に claude -p はDONEが返るまでプロセスを終了しませんでした。ただし今回の実測で分かったのは、「待っている間、メインエージェント自身が先に1つの result を出力している」という点です。ドキュメントは「最終的にプロセスが待つ」ことは説明していますが、「途中経過も result として出力されうる」点までは明記していません。
--forward-subagent-text のドキュメントにあるとおり、parent_tool_use_id を見ればメインとサブエージェントの発言は区別できます。しかし result タイプの行が複数出るケースがある以上、parent_tool_use_id だけでは「これが本当に最後の result か」は判定できません。
実務への影響
claude -p ... --output-format stream-json | jq -r '.result' のように 最初に見つかった result を採用する書き方は、サブエージェントを非同期で使うプロンプトでは意図しない中間報告を拾うリスクがあります。パイプライン側で対応するなら次のいずれかが安全です。
- ストリームを最後まで読み切り、最後に出現した
result行を採用する -
jq -s '.[-1]'のようにJSON配列化してから末尾を取る -
--output-format json(stream-jsonではなく単発JSON)に切り替え、プロセス終了後の1レスポンスだけを扱う設計にする
本プロジェクトの hourly-dispatch のように、Claude自身が別のClaude Codeプロセスをオーケストレーションする構成では、この「result は1回とは限らない」という前提はそのまま設計上の注意点になります。
まとめ
-
--forward-subagent-text(v2.1.211〜)はparent_tool_use_id付きでサブエージェントのthinking/textを stream-json に流す - 非同期サブエージェントを使うと、
claude -pのresultタイプの行は1回とは限らない。今回の検証では2回出力され、本当の最終結果は末尾の行にしかなかった -
claude -pの出力をパースする自動化は「最初のresult」ではなく「最後のresult」を最終値として扱うのが安全
著者視点の発見ポイント
公式ドキュメントは「バックグラウンドサブエージェントの完了をプロセスが待つ」ことまでは明記していますが、「待っている間にメインエージェントの中間発言が独立した result として出力される」点までは記載がありませんでした。実際にコマンドを流して生ログを行ごとに確認するまで、result の出現回数が1回に固定されていないことには気づけませんでした。
関連記事
- Claude Codeの「壊れたループ防止」、実装まで覗いたら効いていた
- claude doctor CLI、3つの警告を出したのに1つも直せなかった
- claude agentsのdraft PR自動化、通知はagent view限定だった
参考リンク
-
Run Claude Code programmatically —
parent_tool_use_idと非同期サブエージェントの待機仕様 -
Week 29 · July 13–17, 2026 —
--forward-subagent-text/CLAUDE_CODE_FORWARD_SUBAGENT_TEXTの追加告知 -
CLI reference —
--forward-subagent-textを含む全CLIフラグ