CC学校

Deep dive · Hooks

ライフサイクルに、
確実な手を入れる。

29 個ある Hook イベントは、すべて「Claude のループのどこかに割り込む」ためのもの。決定論的に必ず動くので、Skill より厳格な強制が必要なときに。

01 / Definition

Hook = 決定論的な拡張ポイント

モデルの裁量に委ねず、毎回必ず発火する。matcher で発火対象を絞り、handler で「何をするか」を書く。

Skill が「Claude が読んで判断して実行する手順書」であるのに対し、Hook は「決まった瞬間に決まったコードを叩く」もの。 ガードレール(block 系)と自動化(format / notify 系)の両方に使えます。

02 / Lifecycle

どのイベントが、いつ走るか

セッションを横軸にした 6 レーン × 29 イベントのタイムライン。クリックすると詳細が出ます。

hook lifecycle · 29 events

セッションのどこで 何が発火する

click events to inspect →

start
·
·
·
end
セッション
プロンプト
ツール実行
サブエージェント
状態監視
コンパクション
その他
ツール実行
PostToolUse

ツール成功後。フォーマッタ・テスト走らせに

approximate position

38% / 100% — セッションの 38% 地点

example

{
  "matcher": "Edit|Write",
  "command": "biome format --write ${FILE}"
}

lang: json

03 / Handlers

ハンドラは 4 種類

シェルコマンド・HTTP・LLM プロンプト・Subagent。組み合わせて使えます。

command

シェルコマンド。最も使われる。stdout / 終了コードで Claude に応答

"command": "biome format --write ${FILE}"
http

外部 webhook を叩く。Slack 通知や社内ロガーに

"url": "https://hooks.example/notify"
prompt

LLM プロンプトを評価させる。多基準の Stop 判定など

"prompt": "テストが追加されているか確認して"
agent

Subagent を起動。重い検証を別文脈で

"agent": "TestRunner"

04 / I/O

入出力スキーマ

stdin で JSON が渡り、stdout / 終了コードで応答。block / inject / replace を制御できます。

hook の挙動
# 入力(stdin に JSON)
{
  "event": "PreToolUse",
  "tool": "Edit",
  "input": { "file_path": "src/api/users.ts", ... },
  "session_id": "...", "cwd": "..."
}

# 出力(exit code)
exit 0  → そのまま許可
exit 2  → ブロック(理由は stderr に書く)
他      → エラー扱い

# 出力(JSON を stdout に出すと、より細かく制御できる)
{ "decision": "block", "reason": "ブランチが main の時は禁止" }
{ "decision": "approve" }
{ "addContext": "ヒント: foo は deprecated です" }   # PreToolUse 等

05 / Recipes

6 つの実戦レシピ

そのまま .claude/settings.json に貼って使える形です。

編集後に biome で自動フォーマット

PostToolUse
{
  "hooks": {
    "PostToolUse": [
      { "matcher": "Edit|Write",
        "command": "biome format --write ${FILE}" }
    ]
  }
}

本番設定への書込を確実にブロック

PreToolUse
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Edit|Write",
        "if": "file matches infra/prod/**",
        "command": "exit 1" }
    ]
  }
}

Stop で Mac デスクトップ通知

Stop
{
  "hooks": {
    "Stop": [
      { "command": "osascript -e 'display notification \"Claude finished\" with title \"Claude\"'" }
    ]
  }
}

編集後にテストを自動実行(async)

PostToolUse (async)
{
  "hooks": {
    "PostToolUse": [
      { "matcher": "Edit",
        "command": "pnpm test --silent",
        "async": true,
        "timeout": 120000 }
    ]
  }
}

コンパクション直前に観点を渡す

PreCompact
{
  "hooks": {
    "PreCompact": [
      { "type": "prompt",
        "prompt": "現在のバグ修正の核心と、まだ未確認の検証手順は必ず残して" }
    ]
  }
}

SessionStart で env 注入

SessionStart
{
  "hooks": {
    "SessionStart": [
      { "type": "json",
        "command": "scripts/load-env.sh",
        "output": "envVars" }
    ]
  }
}

06 / Async

長時間 hook は async に

同期 hook は 30 秒程度の上限あり。テスト実行のような長い処理は async + timeout で。

07 / Debug

動かない時のチェックリスト

hook が動かない時は、たいてい「matcher が合っていない」「JSON が壊れている」「上位で上書き」のどれか。

  1. 1/hooks で登録状況を確認
  2. 2claude settings show --layers で precedence を確認
  3. 3matcher を ".*" に緩めて発火するか試す
  4. 4command を `bash -lc 'echo "$JSON" >> /tmp/hook.log'` に差し替えて入力を可視化
  5. 5exit code 2 を返してみて、Claude 側でブロック扱いになるか確認