Claude Codeの構造化出力とストリーミングをツールに組み込む
claude -pの--output-format json/stream-jsonとjqでの抽出、--include-partial-messagesでのリアルタイム受信、サブエージェント追跡までを実例でまとめます。
はじめに
claude -pの応答は、そのままではただのテキストです。別のツールに読ませたり、ダッシュボードに流し込んだりするには、機械可読な形に変換する必要があります。この記事では、--output-formatでJSON形式の結果を受け取り、--json-schemaで型を固定し、stream-jsonでトークン単位のリアルタイム処理を組む方法をまとめます。
-pモード自体の基本(認証・ツールの自動承認・CIへの組み込み)はClaude Code -pモードでスクリプトやパイプラインを自動化する基本で扱っています。本記事は、その出力を後段のツールへつなぐ部分に絞ります。
前提条件
出力フォーマットの指定は-p実行時のオプションで完結し、追加のインストールは不要です。ただしjqでJSONを整形・抽出する例を使うので、jqコマンドが手元にあると実行結果をそのまま試せます。
出力フォーマットの使い分け
--output-formatは3つの値を取ります。用途で使い分けます。
| 値 | 中身 | 向く場面 |
|---|---|---|
text(既定) | 中身プレーンテキストのみ | 向く場面人が結果を直接読む |
json | 中身resultフィールドにテキスト、session_id等のメタデータを含む単一のJSONオブジェクト | 向く場面スクリプトが結果を1回だけ受け取って処理する |
stream-json | 中身1行1イベントのJSONオブジェクトを逐次出力 | 向く場面リアルタイム処理、進捗の可視化 |
json形式は、コストの追跡にも使えます。応答にtotal_cost_usdとモデルごとの内訳が含まれるので、実行のたびにコストをログへ残せます。これはクライアント側の推定値で、実際の請求額とは差が出ることがあります。
claude -p "このプロジェクトを要約して" --output-format json--json-schemaで型を固定する
--output-format jsonはレスポンスの入れ物をJSONにするだけで、中身のテキストが特定の構造に従う保証はありません。値の構造そのものを固定したいときは--json-schemaにJSON Schemaを渡します。応答のメタデータはそのまま、構造化された値がstructured_outputフィールドに入ります。
claude -p "auth.pyから主要な関数名を抽出して" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}'渡した値がJSON Schemaとして成立していないと、プロンプトを実行する前にError: --json-schema is not a valid JSON Schemaで終了コード1になります。エラーの原因になりやすいパターンと直し方はClaude Code --json-schemaエラーの直し方にまとめているので、検証エラーが出たらそちらを参照します。
Anthropic APIのoutput_config.format(いわゆるstructured outputs)とは指定方法もふるまいも別物です。単発のAPIリクエストで完結するAPI版に対して、-pの--json-schemaはprint modeでの単発実行に対する検証です。API・Agent SDK・Claude Code CLIそれぞれの違いはClaude JSONモードの使い方で詳しく扱っています。
jqでレスポンスを抽出する
json形式の出力はjqで特定のフィールドだけを取り出せます。
# テキスト結果だけを抽出
claude -p "このプロジェクトを要約して" --output-format json | jq -r '.result'
# 構造化出力だけを抽出
claude -p "auth.pyから関数名を抽出して" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
| jq '.structured_output'-rはJSON文字列の前後のダブルクォートを外してそのまま出力するオプションです。ログファイルへの書き出しやシェル変数への代入で使うと扱いやすくなります。
ストリーミングでトークンをリアルタイムに受け取る
stream-jsonに--verboseと--include-partial-messagesを組み合わせると、トークンが生成されるたびにイベントが届きます。ストリームの最後の行は、最終的な応答テキストとコスト、セッションのメタデータを含むresultメッセージです。
claude -p "再帰について説明して" --output-format stream-json --verbose --include-partial-messagesテキストの差分だけを抜き出してそのまま表示する例です。-jは改行を挟まずに結合するオプションで、トークンが途切れず流れて見えます。
claude -p "詩を書いて" --output-format stream-json --verbose --include-partial-messages | \
jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'出力を読み取る側の処理が遅いと、Claude Codeはキューにたまった出力が捌けるまで終了を待ちます。待ち時間は最大30秒までスケールします。大きな応答の末尾が欠ける事故を避けるための仕様です。
サブエージェントのメッセージを追跡する
サブエージェントからのメッセージもassistant・userメッセージとしてストリームに混ざります。見分ける鍵はparent_tool_use_idフィールドです。サブエージェントを起動したツール呼び出しのIDが入り、メインの会話由来のメッセージではnullになります。
既定では、サブエージェントのtool_use・tool_resultブロックだけが流れます。テキストや思考過程まで含めて再構成したい場合は--forward-subagent-textを付けるか、CLAUDE_CODE_FORWARD_SUBAGENT_TEXT環境変数を設定します。有効にすると、サブエージェントがさらに自分のサブエージェントを起動する入れ子構成でも、各階層のメッセージに起動元のツール呼び出しIDが付くので、IDをたどれば入れ子構造を再現できます。
サブエージェントの並列実行そのものの設計はClaude Code Sub-agents完全ガイドで扱っています。
APIリトライイベントを監視する
APIリクエストがリトライ可能なエラーで失敗すると、Claude Codeは再試行の前にsystem/api_retryイベントを出します。進捗をUIに出したり、独自のバックオフを実装したりするときの起点になります。
| フィールド | 内容 |
|---|---|
attempt | 内容現在の試行回数(1始まり) |
max_retries | 内容許容されるリトライの総数 |
retry_delay_ms | 内容次の試行までのミリ秒 |
error | 内容エラー分類(rate_limit・overloaded・server_error等) |
セッション初期化イベントでCIの異常を検知する
ストリームの先頭付近に出るsystem/initイベントには、モデル・使用ツール・接続済みMCPサーバー・読み込み済みプラグインが入ります。CIで「プラグインやMCPサーバーが読み込めていない」ことを検知するゲートに使えます。
plugin_errorsフィールドには読み込み時のエラーが並び、依存バージョンの不一致や--plugin-dirのパス不正が該当します。エラーがあったプラグインはplugins配列から外れ、キー自体が無いのはエラーが0件のときだけです。mcp_server_errorsも同じ構造で、--mcp-configのエントリが検証に失敗すると理由付きで記録されます。この2つのフィールドが空でないときにジョブを失敗させれば、プラグインやMCPサーバーの読み込み失敗をサイレントに進行させずに済みます。
capabilitiesでバージョン差分を吸収する
system/initイベントにはcapabilitiesという文字列配列も含まれます。中身は、そのClaude Codeバージョンが実装しているプロトコル動作の名前です。interrupt_receipt_v1のような値が並びます。
バージョン番号を比較して機能の有無を判定する代わりに、この配列に目的の値が含まれるかで判定します。バージョン文字列の比較はメジャー・マイナー・パッチの表記ゆれに弱く、想定外の分岐を生みやすいためです。知らない値が入っていても無視して構いません。capabilitiesフィールド自体が無い場合は、対応前の古いバージョンだと判断できます。
実践例 — 実行結果を監視するスクリプトを組む
ここまでの要素を組み合わせると、-pの実行を監視する簡単なスクリプトが作れます。ストリームを読みながら、リトライが発生したら標準エラーに警告を出し、最終結果はログファイルに残す構成です。
claude -p "本番デプロイの手順を確認して実行して" \
--output-format stream-json --verbose --include-partial-messages | \
tee raw-stream.jsonl | \
jq -c '
if .type == "system" and .subtype == "api_retry" then
"retry #\(.attempt)/\(.max_retries): \(.error)"
elif .type == "result" then
"done: cost=\(.total_cost_usd // "n/a")"
else
empty
end
'teeでストリーム全体をraw-stream.jsonlに保存しつつ、jq側ではリトライイベントと最終結果だけを抜き出しています。生ログを残しておけば、あとからparent_tool_use_idでサブエージェントの動きを追ったり、system/initのフィールドを見返したりできます。CIのログとして扱うなら、plugin_errors・mcp_server_errorsが空でないときにジョブを失敗させる分岐をここに足せば、プラグインやMCPサーバーの読み込み失敗をリトライ監視と同じ経路で拾えます。
このスクリプトを長時間のジョブに使う場合、パイプ先のjqが固まると出力キューが詰まる点(後述)には注意します。teeを経由させているのは、jq側の処理が万一止まっても、生ログ自体はディスクへ書き出され続けるようにするためです。監視用のパイプラインとログ保存用のパイプラインを分けておくと、片方が詰まってももう片方の記録は残ります。
raw-stream.jsonlをあとから読み返すときは、jq 'select(.parent_tool_use_id != null)' raw-stream.jsonlのようにフィルタすれば、サブエージェント由来のメッセージだけを抜き出せます。障害調査でどのサブエージェントが何をしていたかを追うときに使う手順です。
よくあるつまずき
--json-schemaと--output-format jsonを混同する: 前者は値の構造を検証し、後者は応答の入れ物をJSONにするだけです。スキーマに従った値がほしいのに--output-format jsonだけを指定すると、resultフィールドの中身は自由なテキストのままになります。
ストリームの読み取りをブロックしたまま放置する: jqのようなパイプ先が固まると、Claude Code側の出力キューが詰まります。長時間動かすパイプラインでは、パイプ先の処理が止まらない実装になっているかを確認します。
MCPサーバーの起動待ちを考慮しない: --mcp-configを渡すと、Claude Codeは-pの最初のターンを始める前に、接続待ちのMCPサーバーを待ちます。上限は既定30秒です。リモートサーバーでツール一覧がキャッシュされている場合はこの待ちをスキップし、system/initではpendingのまま最初のツール呼び出し時に接続します。
リトライイベントを最終結果と取り違える: system/api_retryはあくまで途中経過です。最終的な成否は、ストリーム末尾のresultメッセージか、--output-format jsonの終了コードで判定します。
まとめ
出力の解析はスクリプトの見栄えの問題ではなく、実行結果を後段の判断につなげるための土台です。-pの出力を別のツールへつなぐ手段は3段階あります。--output-format jsonでメタデータ付きの単一レスポンス、--json-schemaで値の構造そのものを固定、stream-jsonでトークン単位のリアルタイム処理です。3段階を使い分ける発想さえ押さえれば、あとはCIのゲート・監視ダッシュボード・障害調査ログのどれに使うかで組み方を変えるだけです。サブエージェントの追跡やsystem/initでのCI検証まで含めれば、Claude Codeの実行結果を監視ダッシュボードや後続パイプラインへ組み込む土台がそろいます。claude agents --jsonのようなバックグラウンドセッション管理用のフラグとは別物なので、目的に応じて使い分けます。