Claude Media
Claude Codeの--include-hook-eventsでhookの実行ログを読む

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種類のイベントがストリームに混ざります。

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

フラグは不要です。ストリームに必ず流れます。

hook_startedが出ない

Notification・SessionEnd・PreCompact・PostCompact

フラグを付けても hook_started は出ません。

CLIリファレンスは、後者の例として Notification、SessionEnd、PreCompact、PostCompact を挙げています。「such as」で示されているので、この4つが全部とは限りません。別のイベントで hook_started が見当たらないときは、同じ型の仕様と考えるのが自然です。

hook_started が出ないイベントでも、残りの2種類は条件つきで出ます。

つまり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のイベントが見当たらないときは、次の順で確かめると原因を絞れます。

  1. 出力形式が --output-format stream-json か。テキスト出力では、このフラグは使えません
  2. -p を付けているか。--output-format は印刷モード(-p)向けのオプションです
  3. 対象のイベントが Notification / SessionEnd / PreCompact / PostCompact の仲間か。仲間なら hook_started は出ません
  4. 出ないのが hook_response なら、そのhookが async: true で動いているか。同期のhookでは、この種のイベントに hook_response は流れません
  5. 出ないのが 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が動いていないとは限らないので、監視を組むときは、対象のイベントがどのパターンに当たるかを先に確かめておくと安全です。

この記事を共有:XはてブLinkedIn