Claude Code のセッションでは 30 個の hook イベントが発火する。応答を返せるのは 3 つだけ。

Claude Code の hook イベントの完全な一覧、それぞれがいつ発火するのか、どの 15 個がブロックできるのか、そしてほとんどの hook 出力を黙って飲み込む stdout のルール。数千のエージェントセッションで hook を本番運用して作り上げた実地リファレンス。

Claude Code に hook を組み込むとき、2 つの失敗が何度も繰り返し現れます。そして両者はまったく似ていません。

1 つ目:hook を追加しても、何も起きない。エラーも警告もログ行もない。hook が単に一度も実行されない。

2 つ目:hook は明らかに実行されていて、ディスク上の副作用も見えるのに、エージェントに向けて出力したメッセージが決して届かない。エージェントは、hook が何も言わなかったかのように振る舞う。

どちらも出どころは同じです。hook のシステムは、ほとんどの記事が扱う一握りのイベントよりも大きく、そして一様ではありません。しかも誰がエージェントに話しかけられるかを決めるルールは、直感で当てられるものではありません。これは、私たちが最初から欲しかったリファレンスです。私たちは AgentsRoom をこれらの hook の上に構築しており、以下の内容はすべて、公式リファレンスからの引用か、本番環境で実測したものです。

イベントは 30 個であって、6 個ではない

ほとんどのガイドが扱うのは PreToolUsePostToolUseUserPromptSubmitStopNotificationSubagentStop です。この 6 つは実在し、有用な仕事の大半を担っています。同時に、存在するもの全体の 5 分の 1 でもあります。

完全な一覧を、それぞれが何を観測するかでグループ分けすると次のようになります。

グループイベント
セッションSessionStartSessionEndSetup
プロンプトUserPromptSubmitUserPromptExpansion
ツールPreToolUsePostToolUsePostToolUseFailurePostToolBatch
パーミッションPermissionRequestPermissionDenied
ターンStopStopFailure
サブエージェントとタスクSubagentStartSubagentStopTaskCreatedTaskCompletedTeammateIdle
コンテキストPreCompactPostCompactInstructionsLoaded
環境FileChangedCwdChangedConfigChange
WorktreesWorktreeCreateWorktreeRemove
インターフェースNotificationMessageDisplay
MCP elicitationElicitationElicitationResult

エージェントのセッション中に Claude Code の 30 個の hook イベントが発火する順序を示したタイムライン図。SessionStart から UserPromptSubmit、PreToolUse、PostToolUse、Stop を経て SessionEnd まで、どのイベントがエージェントをブロックできるかも示されている。

1 つのセッションでイベントが発火する順序。ツールのブロックはツール呼び出しごとに繰り返され、プロンプトのブロック全体はターンごとに繰り返されます。

このうちいくつかは、システムの捉え方そのものを変えます。PostToolUseFailure が存在するということは、「ツールは動いたのか」という分岐がイベントであって、ペイロードから推測するものではないということです。PostToolBatch は並列のツール呼び出しのバッチが解決したあとに 1 回だけ発火するので、編集ごとに 1 回ではなく、linter を 1 回だけ走らせるのに適した場所です。InstructionsLoadedCLAUDE.md が読み込まれたときに発火するので、あなたが読み込まれていると思っているルールをエージェントが本当に読み込んだのかどうかを確認する hook ポイントが手に入ります。

ほとんどの hook 出力を飲み込む stdout のルール

このページで最も役に立つのは、この一点です。

終了コード 0 のとき、Claude Code は stdout を解析して JSON の出力フィールドを探します。しかし、その stdout がエージェントに見せられるかどうかはイベント次第で、例外は短いリストに収まります。リファレンスにはこうあります。

ほとんどのイベントでは、stdout は debug ログに書き出されますが、トランスクリプトには表示されません。例外は UserPromptSubmitUserPromptExpansionSessionStart で、これらでは stdout が、Claude が見て行動に移せるコンテキストとして追加されます。

30 個のうち 3 個です。PostToolUse の hook から echo "warning: this migration is destructive" を実行し、エージェントがそれを読んでくれると期待しても、決して読まれません。あなたのテキストは debug ログに行きました。

ほかのイベントからエージェントの目の前にテキストを届ける方法は、ちょうど 2 つあります。

  1. 終了コード 2 で stderr に書き出す。 終了コード 2 のとき、Claude Code は stdout とその中の JSON をすべて無視し、stderr をエラーメッセージとしてエージェントに返します。
  2. 終了コード 0 で JSON オブジェクトを出力する。 そこに hookSpecificOutput.additionalContext を載せます。

1 つ目の非対称性に注目してください。終了コード 0 では stdout が意味を持ち、stderr は持ちません。終了コード 2 では stderr が意味を持ち、stdout は完全に捨てられます。これを逆に理解していることが、hook が完全に正しく見えるのに黙ったままになる理由です。

それ以外の終了コードは、ブロックしないエラーです。トランスクリプトには stderr の 1 行目を添えた <hook name> hook error の通知が表示され、実行は継続し、stderr の全文は debug ログに落ちます。

Claude Code の hook の終了コードを示した図。終了コード 0 では stdout の JSON が 3 つのイベントでのみエージェントに届き、終了コード 2 ではアクションがブロックされて stderr がエージェントに届き、それ以外の終了コードは debug ログに書き出されるブロックしないエラーになる。

終了コードごとに、どのチャネルがエージェントに届くか。破線の経路は、あると思われがちで実際には存在しないものです。

ちょうど半分がブロックできる

15 個のイベントは終了コード 2 でアクションを止めます。15 個はそれを無視して進みます。

ブロックできる: PreToolUsePermissionRequestUserPromptSubmitUserPromptExpansionStopSubagentStopTeammateIdleTaskCreatedTaskCompletedConfigChangePostToolBatchPreCompactElicitationElicitationResultWorktreeCreate

ブロックできない: PostToolUsePostToolUseFailurePermissionDeniedStopFailureNotificationSubagentStartSessionStartSetupSessionEndCwdChangedFileChangedPostCompactWorktreeRemoveInstructionsLoadedMessageDisplay

実務上の帰結:ガードレールは PreToolUse に置くものであって、PostToolUse には決して置きません。PostToolUse はツールが成功したあとに発火します。そこで終了コード 2 を返しても書き込みは取り消されず、被害がディスク上に残ったままエラーを表示するだけです。rm -rf を止めたいなら、それができる場所はちょうど 1 か所です。

PostToolUse はブロックしないのに PostToolBatch はブロックする、という点はもう一度見ておく価値があります。これは、並列の編集が着地したあとでもバッチレベルのチェックがターンを止められるということであり、このシステムが提供するもののなかで、書き込み後の拒否権に最も近いものです。

matcher は完全一致、そして突然そうでなくなる

matcher フィールドは、自分自身が含む文字によって評価戦略を切り替えますが、どちらの経路に入ったのかは何も教えてくれません。

matcher評価のされ方
"*"""、または省略すべてに一致
英字、数字、_-、スペース、,| のみ完全一致の文字列、または |, で分割された完全一致の文字列のリスト
それ以外アンカーなしの JavaScript 正規表現

罠になるのは、アンカーなしという点です。リファレンスは、正規表現が RegExp.prototype.test でテストされると明示しています。これは値のどこかに一致すれば成功します。したがって Edit.*Edit NotebookEdit の両方に一致します。1 つのツールだけを指したかったのなら、^Edit$ と書いてください。

見当違いのデバッグを始める前に知っておきたい、バージョン依存の挙動が 2 つあります。

  • カンマ区切りと空白の許容には、Claude Code v2.1.191 以降が必要です。
  • ハイフンが完全一致の文字集合に加わったのは v2.1.195 です。それ以前は、code-reviewer のような matcher はアンカーなしの正規表現として扱われ、senior-code-reviewer に対しても発火していました。

AgentsRoom では、自分たちのファイル帰属 hook を Write|Edit|MultiEdit|NotebookEdit で絞っています。これは完全一致の文字列の経路に留まり、この 4 つのツールだけに一致してほかには一致しません。私たちがインストールするライフサイクルの hook には matcher が一切ありません。常に私たちに関係するイベントだからです。

hook を定義できる場所は 6 つあり、それらはマージされる

本能的には優先順位を探したくなります。優先順位は存在せず、そこが面白いところです。

場所スコープ
~/.claude/settings.jsonあなたのすべてのプロジェクト、マシンローカル
.claude/settings.json1 つのプロジェクト、コミット可能
.claude/settings.local.json1 つのプロジェクト、Claude Code が gitignore する
Managed policy settings組織全体、管理者が制御
プラグインの hooks/hooks.jsonプラグインが有効な間
skill または agent の frontmatterそのコンポーネントが有効な間

リファレンスにはこうあります。

hook のエントリは、settings のレベルをまたいで互いを置き換えるのではなくマージされます。ユーザー、プロジェクト、ローカルの settings は、管理された hook を取り除くことなく自分自身の hook を追加し、disableAllHooks の設定は managed settings の外から managed hook を無効化できません。

つまり、プロジェクトの hook がグローバルな hook を上書きすることは決してなく、その上に積み重なります。6 つのソースは、すべて加算的です。ユーザー settings で定義した PostToolUse のフォーマッタをプロジェクトでもう一度定義すると、編集ごとに 2 回実行され、唯一の症状は「なんとなく遅い」という感覚だけです。

Claude Code の hook を定義できる 6 つの settings の場所を示した図。すべてが互いを上書きするのではなく、加算的にマージされて 1 つの hook のセットになる様子を表している。

6 つのソース、1 つのマージされたセット。ここでは何も、ほかの何かを上書きしません。

これは、あるツールが誰かのプロジェクトに hook をインストールする場所として .claude/settings.local.json が正しい理由も説明します。プロジェクト単位のスコープで、Claude Code がこれを gitignore し、CLI のフラグなしで読み込まれます。AgentsRoom がエントリを書き込むのはそこです。そのおかげで、ユーザーがコミットしている .claude/settings.json は決して触られず、同僚がマシン固有のパスを引き継ぐこともありません。

本番で hook を動かして学んだこと

AgentsRoom は、開いたすべてのプロジェクトに hook をインストールします。エージェントの状態を決定論的に追跡し、編集されたファイルを正しいエージェントに帰属させるためです。その規模になって初めて見えてくることがいくつかあります。

未知のイベント名は黙って無視される。 これはドキュメントに書かれていませんが、私たちはこれに依存しています。インストーラに新しいライフサイクルイベントを追加すると、古い CLI を使っているユーザーの手元には、そのバイナリが一度も聞いたことのないイベント名を含む settings.local.json が置かれます。何も壊れず、何も警告されず、そのエントリはスキップされます。CLI のリリースに先んじてインストーラを出荷しても安全なのは、これがあるからです。そして必然的に、つづり間違いがエラーではなく完全な沈黙を生むのも、これが理由です。

agent_id は、自分がサブエージェントの中にいることを知る手段。 このフィールドは、hook がサブエージェントの呼び出しの中で発火したときにだけ存在します。これは聞こえる以上に重要です。Stop は、メインエージェントだけでなく、サブエージェントがターンを終えたときにも発火します。「Stop が来たらセッションを完了とマークする」という素朴なルールは、どれか 1 つのサブエージェントが最初に戻ってきた時点で、セッション全体を完了にしてしまいます。私たちが agent_id を持つターン系イベントをスキップしているのは、まさにこの理由からです。

現在のターンについて transcript_path を読まないこと。 リファレンスは、トランスクリプトが非同期に書き出されるためメモリ上の会話に遅れることがあり、hook が発火した時点では最新のメッセージがまだそこにない可能性がある、と警告しています。StopSubagentStoplast_assistant_message を受け取るのは、ファイルとの競争をしなくて済むようにするためです。

hook は唯一の信頼できる状態シグナル。 hook 以前、私たちはエージェントが考えているのか、待っているのか、終わったのかを割り出すために PTY をスクレイピングしていました。それは、CLI が端末の代替スクリーンバッファ経由で描画した瞬間に壊れます。/tui fullscreen がやっているのが、まさにそれです。hook はどのレンダラーの下でも同じように発火します。外側からエージェントを観測する何かを作っているなら、これこそ土台にすべきレイヤーであり、スクレイピングはよくてもフォールバックどまりです。

async: true はコストゼロ。 hook のコマンドは async: true を宣言でき、エージェントはそれを待ちません。私たちの hook は 2 秒の上限を付けてローカルのエンドポイントに POST し、すぐに戻ります。受け取り側のアプリが閉じているときでも、エージェントのターンのレイテンシは影響を受けません。hook が観測するだけで何も決定しないなら、async にして、そのコストを払うのはやめましょう。

hook に端末へゴミを書かせないこと。 私たちのスクリプトは、トップレベルも含めてあらゆる例外を飲み込みます。hook から出た未処理のトレースバックは、静かに失敗するだけでは済みません。ユーザーが作業している最中の端末セッションに、Python のスタックトレースを印字します。

タイムアウト

デフォルトは寛大ですが、そうでない例外が 3 つあります。

hook の種類デフォルトのタイムアウト
commandhttpmcp_tool600 秒
prompt30 秒
agent60 秒
UserPromptSubmit(command, http, mcp_tool)30 秒
MessageDisplay(command, http, mcp_tool)10 秒
SessionEndすべての hook で共有される 1.5 秒。hook ごとの timeout がより長ければ、それに合わせて最大 60 秒まで引き上げられる

驚かれるのは SessionEnd の予算です。これは hook ごとの割り当てではなく共有の予算なので、明示的に引き上げない限り、3 つのクリーンアップ hook は 1.5 秒を分け合うことになります。

短くまとめると

  • イベントは 30 個ある。有名なのは 6 個。
  • stdout がエージェントに届くのは UserPromptSubmitUserPromptExpansionSessionStart だけ。それ以外の場所では、終了コード 2 と stderr を使うか、JSON の additionalContext を使う。
  • 15 個のイベントは終了コード 2 でブロックし、15 個はそれを無視する。ガードレールは PreToolUse に置く。
  • matcher は完全一致の文字列で、特殊文字が 1 つ入った時点でアンカーなしの正規表現に変わる。
  • 6 つの settings のソースは加算的にマージされる。何も、ほかの何かを上書きしない。
  • イベント名のつづり間違いは、完全な沈黙のなかで失敗する。

これらのイベントについて頭で推論するのではなく、実際に発火するところを見たいなら、私たちが作ったのはまさにそれです。AgentsRoom は、数十の並列エージェントサブエージェントのセッションにまたがって、エージェントごと、プロジェクトごと、実行ごとに、すべての hook のトリガーを表示します。あなたが自分の settings で設定した hook は、書いたとおりにそのまま動き続けます。AgentsRoom が本物の CLI を実行しているからです。

続きを読む

AgentsRoomをダウンロード

あなたのAIエージェント(Claude、Codex、Antigravity CLI、OpenCode、Aider、Grok Build、Mistral Vibe、Kimi Code)を単一のウィンドウから実行します。

無料AgentsRoomをダウンロード

コンパニオンアプリ:外出先でもエージェントを確認

Claude、Codex、Antigravity CLI、またはその他の AI プロバイダーを使用します。

拡張機能を入手
Chrome Web Store

バグや要望を公開バックログに直接送信できます。

実際の AgentsRoom の様子。

マルチプロジェクト
マルチプロバイダー
マルチエージェント
ライブステータス
ファイル差分
モバイルアプリ
ライブプレビュー
エージェントチーム
ブラウザテスト
バックログ駆動開発
プロンプトライブラリ
スキルライブラリ
すべての機能を見る