Claude Codeの--include-hook-eventsでhookの実行ログを読む
--include-hook-eventsで出力ストリームに流れるhook_started・hook_progress・hook_responseの意味と、イベントごとに出る・出ない条件をまとめます。
--include-hook-events は、hookの実行状況を出力ストリームのイベントとして流すフラグです。CIや自作ツールから claude -p を呼ぶとき、どのhookがいつ動き、どう終わったかをJSONで追えるようになります。ただし、すべてのhookが同じ形で流れるわけではありません。この記事では、イベントごとに何が出て何が出ないかを表にまとめます。
--include-hook-eventsで出力ストリームに加わるもの
このフラグを付けると、hookのライフサイクルを表す3種類のイベントがストリームに混ざります。
hookのライフサイクルイベント
hook_started
hookが実行を始めたときに出ます。
hook_progress
hookの実行中に、stdoutやstderrの出力が届いたときに出ます。
hook_response
hookが終了したときに出ます。終了コードと結果の区分(outcome)が付きます。
使う条件は1つだけで、--output-format stream-json が必要です。-p のテキスト出力やjson出力では使えません。
claude -p "このリポジトリの概要を教えて" \
--output-format stream-json --verbose \
--include-hook-events--verbose は、CLIリファレンスの用例とヘッドレス実行の説明が stream-json と組み合わせて載せているので、同じ形に揃えておくと迷いません。出力は1行1JSONで、hookのイベントは type が system で subtype が hook_ で始まる行として届きます。
3つのイベントが持つフィールド
Agent SDK(TypeScript)の型定義に、3種類のフィールドが載っています。共通するのは hook_id、hook_name、hook_event、uuid、session_id です。hook_event にはSessionStartやPostToolUseといったイベント名が入ります。
| イベント | 固有のフィールド |
|---|---|
hook_started | 固有のフィールドなし(共通フィールドのみ) |
hook_progress | 固有のフィールドstdout / stderr / output |
hook_response | 固有のフィールドoutput / stdout / stderr / exit_code(省略可)/ outcome |
outcome は success / error / cancelled のいずれかです。exit_code は省略可能な項目なので、終了コードが取れない終わり方を想定して読む必要があります。
同じ hook_id で3つのイベントがつながります。複数のhookが並行して動いても、hook_id で束ねれば1つのhookの経過を再構成できます。
フラグなしでも流れるイベントと、流れないイベント
ここが仕様の核心で、イベントの種類によって扱いが分かれます。
イベントごとの扱いの違い
SessionStartとSetup
フラグは不要です。ストリームに必ず流れます。
Notification・SessionEnd・PreCompact・PostCompact
フラグを付けても hook_started は出ません。
CLIリファレンスは、後者の例として Notification、SessionEnd、PreCompact、PostCompact を挙げています。「such as」で示されているので、この4つが全部とは限りません。別のイベントで hook_started が見当たらないときは、同じ型の仕様と考えるのが自然です。
hook_started が出ないイベントでも、残りの2種類は条件つきで出ます。
hook_progress: 1秒を超えて動くcommand hookが出力を出したときに出るhook_response: バックグラウンドで動くhookが終了したときだけ出る
つまりNotificationのhookをふつうに同期で動かしても、hook_response は流れません。hookの完了を確かめたい用途では、通知系hookを async: true にしない限り、ストリームからは終了を読み取れないことになります。
SessionStartとSetupの結果をストリームで読む
SessionStartとSetupは、フラグなしで流れます。ヘッドレス実行の説明では、これらのイベントは system/init より前に届くとされています。
Setupは通常の起動では動かず、起動フラグによって発火するmatcherが決まります。
| 起動方法 | 発火するmatcher |
|---|---|
claude --init-only / claude -p --init | 発火するmatcherinit |
claude -p --maintenance | 発火するmatchermaintenance |
Setupのhookは出力を捨てる仕様で、-p のときにstdout、stderr、終了コードが現れる場所は hook_response だけです。
claude -p --init "依存関係は入った?" \
--output-format stream-json --verbose正常に動いたかは、この出力の hook_response にある exit_code と outcome で確認できます。
--init-only はSetupと、startup matcherのSessionStartのhookを動かしたあと、会話を始めずに終了し、成功時には何も表示しません。-p で会話を始めるときは、引数かstdinでプロンプトを渡す必要があります。例外は、SessionStartのhookが initialUserMessage を返す場合と、保留したツール呼び出しを再開する場合です。
Setupで動くのは type: "command" のhookだけで、type: "mcp_tool" のhookは常にスキップされます。hook_response が出てこないSetupのhookがあれば、まずhookの種類を疑ってください。
配信タイミングにも履歴があります。v2.1.169からv2.1.203では、これらのイベントがhookの完了後にまとめて届いていました。v2.1.204で、hookが出力するたびに流れる形に戻っています。進捗をリアルタイムに表示するラッパーを書いているなら、v2.1.204以降のClaude Codeで試してください。
jqでhookの失敗だけを拾う
次のjq式は、失敗したhookだけを1行ずつ抜き出す例です。フィールド名は型定義に従っていますが、これは出力そのものではなく、書き方の一例です。
claude -p "テストを実行して" \
--output-format stream-json --verbose --include-hook-events \
| jq -c 'select(.type == "system" and .subtype == "hook_response"
and .outcome != "success")
| {hook_name, hook_event, exit_code, outcome}'CIに組み込むときは、これを終了判定に使えます。たとえばPostToolUseのlint hookが error で終わったら、後続のステップを止める、といった運用です。cancelled は別に扱うほうが安全です。理由は次の節のバックグラウンドhookにあります。
バックグラウンドhookのhook_responseが出る条件
async: true のhookは、Claudeの実行を止めずに裏で動きます。終了時に結果を受け取れるのは、実行中のセッションが生きているときだけです。
-pでは、終了時点でまだ動いているasync hookを強制終了し、outcomeをcancelledとして記録する- hookの仕事が
claude -pより長く続く必要があるなら、hookから完全に切り離したプロセスを起動する
async を指定できるのは type: "command" のhookだけです。次の設定は、Writeツールの実行後にテストを裏で走らせる例で、終了すると hook_response が流れます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "./run-tests.sh", "async": true }
]
}
]
}
}裏で動き出したasync hookには timeout が適用されません。つまり、終わらないスクリプトを async: true で置くと、-p が終わるまで hook_response が届かず、最後に cancelled で締められます。async のhookが返す decision や continue のようなフィールドも、動作を止める効果を持ちません。
async hookが終了後にJSONで返す additionalContext と systemMessage は、次の会話ターンでClaudeに渡されます。同期hookの systemMessage と違い、どちらも画面には表示されません。型が合わないフィールドは捨てられ、--debug を付けると捨てられたフィールドの名前が警告に出ます。v2.1.202より前は、async hookが壊れたJSONを返すとセッションが落ちることがあり、再開のたびに再発していました。
stream-jsonでasync hookを監視するなら、hook_started と hook_response の組み合わせで「始まったが終わっていない」ものを数えられます。ただしNotificationなどの hook_started を出さないイベントでは、この数え方が成り立ちません。
流れてこないときの切り分け
期待したhookのイベントが見当たらないときは、次の順で確かめると原因を絞れます。
- 出力形式が
--output-format stream-jsonか。テキスト出力では、このフラグは使えません -pを付けているか。--output-formatは印刷モード(-p)向けのオプションです- 対象のイベントが
Notification/SessionEnd/PreCompact/PostCompactの仲間か。仲間ならhook_startedは出ません - 出ないのが
hook_responseなら、そのhookがasync: trueで動いているか。同期のhookでは、この種のイベントにhook_responseは流れません - 出ないのが
hook_progressなら、command hookが1秒を超えて動き、かつ出力を出したか
たとえば、Notificationに通知を鳴らすhookを入れた場合を考えます。
{
"hooks": {
"Notification": [
{
"hooks": [
{ "type": "command", "command": "./notify.sh" }
]
}
]
}
}この設定を同期のまま動かすと、hook_started は出ず、hook_response も出ません。notify.shが1秒を超えて出力を出したときだけ、hook_progress が見えます。hookが実際に動いたかを確かめたいなら、スクリプト側でログファイルに書き込むか、--debug-fileのログを見るほうが確実です。
SessionEndにも注意点があります。SessionEndのhookは1.5秒の時間枠を共有しており、settingsでhook単位のtimeoutに長い値を指定すると、その長さに合わせて枠が広がります(上限は60秒)。短い枠のなかで動くhookなので、終了処理の結果をストリームで読み取る設計にはしないほうが無難です。
使い分けの目安
| やりたいこと | 使う手段 |
|---|---|
| SessionStart / Setupの成否だけ見たい | 使う手段フラグなしで足りる |
| PreToolUseなど全イベントの経過を追いたい | 使う手段--include-hook-events |
| hookの内部ログまで追いたい | 使う手段--debug-fileでログを残す |
| SDKから同じ情報を受け取る | 使う手段includeHookEvents オプション |
SDKのオプション名は includeHookEvents で、既定は false です。説明の文面もCLIとほぼ同じなので、CLIで挙動を確かめてからSDKへ移すと、取りこぼしの原因を切り分けやすくなります。出力形式そのものの扱いは、-pモードの基本と、起動失敗をresultメッセージで受け取る方法にも関連の話があります。
まとめ
--include-hook-events は、hookの開始・途中経過・終了をストリームに流すフラグです。ただし、イベントによっては hook_started が出ず、hook_response はバックグラウンドhookの終了時だけに限られます。ストリームが静かだからといってhookが動いていないとは限らないので、監視を組むときは、対象のイベントがどのパターンに当たるかを先に確かめておくと安全です。