原典: React to events / 日本語版

一行要約

mod の hook は Claude Code が動く前に走るミドルウェアの鎖で、見る(next(e) を返す)・書き換える(変えたコピーを next に渡す)・自分で答える(next を呼ばない)のどれかを選ぶ。止める用途の hook は、失敗すると既定で素通り(fail open)になるので .catch で fail closed にする。

要点

3つの扱い方

  • observe —— 自分の処理をして next(e) を返す。後で何かするなら await next(e) してから
  • rewrite —— イベントは深く凍結されていて、代入すると例外。変えたコピーを next に渡す。結果のほうを変えるなら await next(e) のあとでコピーを返す
  • answer —— next を呼ばずに結果を返す。鎖が切れ、後の mod も Claude Code 自身の動作も走らない

matcher(on の第2引数)でイベントを絞れる。値・配列・正規表現が使え、全フィールドが一致したときだけ走る。同じイベントを matcher 無しで2回登録すると、モジュールが読み込まれない。

ツール呼び出しを守る・変える(tool.call)

subagent と MCP ツールの呼び出しも含め、ツールを走らせる直前に発火する。next(e) を呼ぶと、Claude Code が permission の検査をしてからツールを走らせる。

When you do that, no permission prompt appears and the tool doesn’t run, so the result you return is all Claude learns about the call.

(next を呼ばずに { result: ... } を返すと)確認プロンプトは出ず、ツールも走らない。返した結果が、その呼び出しについて Claude が知るすべてになる。

{ deny: '...' } を返すと、Claude はその文をツールの結果として読む —— Claude が行動できる指示として書く。

managed settings の hook は、どの mod の tool.call よりも先に走り、そのブロックは最終。

ユーザーが決めるまで保留する —— hook は next を呼ぶ前に await でき、その間ツール呼び出しは保留される。$.ui.ask で選択肢を出す。待つのは mods API の呼び出しの中で —— その時間は hook の時間制限に数えられないが、自前の promise を待つ時間は数えられ、時間切れの hook は飛ばされて保留中のコマンドが走る。claude -p では $.ui.ask が reject する。

承認・拒否を決める(tool.check)

It fires after the permission rules and the settings hooks have decided, and next(e) resolves to their decision: allow, ask, or deny. Your hook returns that decision or a different one.

permission ルールと settings hook が決めた後に発火し、next(e) はその決定(allow / ask / deny)に解決される。hook はその決定か、別の決定を返す。

固定のコマンドやパスなら permission ルールで足りる。判断がその時点の状態(今の git ブランチ、別の hook が記録した値)に依存するときに使う。PreToolUse hook(managed 以外)が止めた呼び出しも承認できる(どの決定が mod に勝つかは permissions)。

The hook matches the text of the command, so treat it as a reminder for Claude. To block pushes to main for everyone, protect the branch on your Git host.

この hook はコマンドの文字列に当てているので、Claude への注意喚起として扱え。全員に対して main への push を止めたいなら、Git のホスト側でブランチを保護せよ。

プロンプトとターン

  • prompt.submit —— next({ ...e, text }) で書き換え(transcript にも新しい文が出る)、context に足すと Claude だけが読む文を後ろに付けられる、{ drop: '...' } で送らない。system prompt の節や最初の context、skill の文を変える hook の出力が、リクエストごとに変わると prompt cache が壊れる
  • turn.start / turn.step / turn.complete —— turn.step はモデルへのリクエストごと(ツール呼び出しのあるターンは複数回)。トークン使用量を読む、next({ ...e, model }) で別のモデルに送る、モデルを呼ばずに答えるができる。ストリームするので async generator で書き、yield* next(e)。subagent のリクエストにも走るので、メインだけなら e.agentId を見る
  • settings hook のイベントは classic.Stop のように classic. を付けた名前で扱える

mod が走る順序

鎖の外側が先に見て、結果を後で見る。後の mod は前の mod がイベントを見るのを止められない。

  1. 組み込みのガード sec-default@builtin(読み込まれる環境で)、組織の prependPlugins、その他の組織の mod
  2. ユーザーが入れた mod(dependencies に挙げた mod より先)
  3. 組織の appendPlugins
  4. その他の組み込み mod

settings hook の位置: managed の PreToolUse hook は最初の mod の tool.call より前で、ブロックは最終(どの mod も見ない)。それ以外の設定ファイルと plugin の PreToolUse hook は、最後の mod が next を呼んだ後に Claude Code 自身の動作の一部として走る —— next を呼ばずに答える mod はそれらを走らせない。 tool.check はそれらの後に発火するので、それらが止めたものも承認できる。

hook が失敗したら

It failed before calling next: Claude Code skips it, and the next handler runs in its place

next を呼ぶ前に失敗した場合: Claude Code はそれを飛ばし、次の handler が代わりに走る

例外・時間切れ・形の違う結果で失敗した hook は、next を呼ぶ前なら飛ばされて素通りする(next が解決した後なら、その結果が残る)。呼び出しを止める hook は .catch を付け、失敗したら { deny } を返して fail closed にする —— 付けないと、守っていたコマンドが走る。

そのまま使える具体例

# 失敗したときの1行(どこで読めるかはセッションによる)
my-mod: tool.call hook skipped: threw Error: boom

原典のコード例は次の形を示している(コードは原典を見る): git push --force を deny で止める tool.call、.mdx の編集を $.ui.log で記録する tool.call、危険なコマンドを $.ui.ask で保留する tool.call、main での git push を拒否する tool.check、PR に触れたプロンプトに今のブランチを足す prompt.submit、リクエストごとの cache 読み書きを記録する turn.step、.catch で fail closed にする guard。

原典で言及されている関連文書