「commit 前に lint かけ忘れて、CI で慌てた」「危険な rm -rf を Claude にうっかり走らせかけた」「セッション開始のたびに git status を貼り直している」。Claude Code を毎日使い始めると、こうした 「毎回手動で気づかせる作業」 が静かに積み上がります。本記事ではその出口になる Hooks(公式 reference)を、ライフサイクル全体図と最小実装の両方で扱います。
シリーズ #10 API を curl で叩く生活から卒業する で MCP を扱いました。本記事は同じ「Phase D:外との接続」の中で、外との接続ではなく Claude Code 自身のライフサイクルに割り込む レイヤーです。
TL;DR
- Hooks は Claude Code のライフサイクル特定地点で自動発火するスクリプト機構(公式 reference)
- 公式 docs の Hook lifecycle 表には 2026-05-16 時点で 29 イベントが列挙されている(該当セクション)
- 発火頻度を 3 つ(セッションあたり / ターンあたり / ツール呼び出しあたり)に分けて整理すると頭に入る
- 4 つの handler type(command / prompt / agent / http)。日常運用は
commandが中心 - matcher(hook を発火させる条件フィルタ)の型は hook イベントごとに違う。
/hooksメニューで確認できる - 今週やる 1 つ:commit 前 lint の PreToolUse hook を
if: "Bash(git commit*)"で 15 行書く
もし、Claude が commit 前に勝手に lint してくれたら
「先に lint かけてからコミットして」と毎回プロンプトに書いている人は、私だけではないと思います。Claude Code を使い始めた最初の月、私は次のような体験を 1 日に何度か繰り返していました。
- 急いでいるときに「コミットして」とだけ送ってしまい、lint エラー入りで push してしまう
- 危険な
rm -rfをうっかり Claude に走らせかけて、/permission askで止まったから事なきを得る - 毎朝セッションを開くたびに、
git statusと直近の作業メモを手で貼り直す
要するに「Claude にいつも同じ前提を、自分の頭から呼び出してプロンプトに書く」往復で時間が溶けます。もし Claude Code が、commit 前に必ず lint をかけてくれて、危険なコマンドを未然に止めてくれて、セッション開始時に作業前提を勝手に注入してくれたら ─ 便利だと思いませんか?
これを叶える仕組みが Hooks です。
問題の立て方が間違っている
「毎回プロンプトに 先に lint かけて と書く」を改善しようとすると、たいてい「短い CLAUDE.md にこのルールを書いておこう」「/morning-check のような slash command にまとめよう」という方向に行きます。どちらも有効ですが、いずれも Claude がそれを読みに行く必要があります。Claude が忘れたら効きません。
Hooks は逆向きです。Claude Code 本体がライフサイクルの特定の瞬間に、こちら側のスクリプトを 強制的に呼びに来ます。Claude の判断を経由しません。だから「Claude が忘れる」という事象自体が消えます。
書く場所も settings.json の hooks キーで、シリーズ #09 設定階層を理解すれば事故らない で扱った 5 階層スタックにそのまま乗ります。チーム共有なら .claude/settings.json、自分専用なら .claude/settings.local.json、全プロジェクトなら ~/.claude/settings.json です。
Hooks の全体像 ─ 29 イベントを 3 つの発火頻度で整理する
公式 docs の Hook lifecycle 表(2026-05-16 時点)には 29 種類のイベントが並んでいます。一気に覚えようとすると挫折するので、公式自身が冒頭文で示している 3 つの発火頻度(公式英語表記は cadence)で頭に入れます。公式から原文どおり引用します。
Events fall into three cadences: once per session (
SessionStart,SessionEnd), once per turn (UserPromptSubmit,Stop,StopFailure), and on every tool call inside the agentic loop (PreToolUse,PostToolUse):イベントは 3 つの発火頻度に分かれます。セッションあたり 1 回(
SessionStart、SessionEnd)、ターンあたり 1 回(UserPromptSubmit、Stop、StopFailure)、そしてエージェントループの中のツール呼び出しごとに 1 回(PreToolUse、PostToolUse)。
この 3 帯のどこに自分の hook を置きたいかを決めれば、残り 23 種類は「派生・側枝」として整理できます。
flowchart TD
subgraph S["per session(外側 — 1 セッションで 1 回)"]
S1["SessionStart"]
subgraph T["per turn(中 — 1 セッション中に複数回)"]
T1["UserPromptSubmit"]
subgraph A["per tool call(内側 — 1 ターン中に何度も発火)"]
A1["PreToolUse"]
A2["PostToolUse"]
end
T2["Stop"]
end
S2["SessionEnd"]
end
S1 --> T1
T1 --> A1
A1 --> A2
A2 --> T2
T2 --> S2session の枠(外側)の中に turn の枠(中)、さらにその中に tool call の枠(内側)がネストしています。1 セッション中に turn は何度も繰り返され、1 ターン中に tool call は何度も発火する、という入れ子の発火頻度です。残り 23 イベント(side-effect 系・subagent / task / compact / worktree / MCP elicitation)はこの 3 帯の派生・側枝で、必要になったときに下の表から引きます。
公式の Hook lifecycle 表に列挙されている 29 イベントを、発火頻度別にまとめます。
| 発火頻度 | イベント |
|---|---|
| per session | SessionStart / Setup / SessionEnd |
| per turn | UserPromptSubmit / UserPromptExpansion / Stop / StopFailure |
| per tool call | PreToolUse / PermissionRequest / PermissionDenied / PostToolUse / PostToolUseFailure / PostToolBatch |
| side-effect 系 | Notification / ConfigChange / CwdChanged / FileChanged / InstructionsLoaded |
| subagent / task | SubagentStart / SubagentStop / TaskCreated / TaskCompleted / TeammateIdle |
| compact | PreCompact / PostCompact |
| worktree | WorktreeCreate / WorktreeRemove |
| MCP elicitation | Elicitation / ElicitationResult |
29 イベントの全数と各イベントの input schema は公式 Hooks reference をご確認ください。
4 つの handler type ─ 何が起こるかを決める「中身」の選択
イベントが発火したときに 何が走るか は handler の type で決まります。公式 docs では 4 種類が定義されています(Hook handler fields)。
| type | 仕組み | 主な用途 |
|---|---|---|
command | シェルコマンドを実行。stdin で JSON 受け取り、exit code と stdout で返す | lint・format・通知音。日常はほぼこれ |
prompt | Claude モデルに 1 ターン判定させ、{"ok": true/false, "reason": "..."} を返す | コミットメッセージ規約のような「ルールでは書ききれない判断」 |
agent | subagent を spawn(独立した子セッションとして新しく立ち上げる)して多ターン検証(Read / Grep / Glob 使用可) | テストが通っているか実ファイルで確認する必要があるとき |
http | URL に JSON POST して応答を受け取る | 外部 webhook 連携・社内承認システムへの問い合わせ |
最初に書くべきは command です。prompt と agent は「シェルでは判断しきれないが、Claude の判断は呼びたい」というレアケースで、http は v2.1.63 で追加された比較的新しい手段です( Anthropic 公式 CHANGELOG.md で確認できます)。
command 型の最小形は 4 行で書けます。
{
"type": "command",
"command": "echo 'tool called' >> /tmp/claude.log",
"timeout": 5000,
"async": true
}
timeout はミリ秒。async: true を付けると Claude Code 本体をブロックしません。lint や format のような「結果を後で見ればよい」処理は async、block / deny を返したい処理は同期 が原則です。
Hook が発火するまでの流れ
settings.json に書いた hook が、実際に発火するまでの中の流れはこうなります。
flowchart TD
A["Claude Code 起動"] --> B["settings.json 5 階層を読む<br/>(Managed → CLI → local → project → user)"]
B --> C["hook イベントが発生<br/>(PreToolUse, SessionStart, ...)"]
C --> D{matcher 評価}
D -->|一致しない| E["何も起きない"]
D -->|一致する| F{handler type}
F -->|command| G["シェル実行<br/>stdin で JSON 受け取り"]
F -->|prompt| H["Claude にプロンプト送信"]
F -->|agent| I["subagent を spawn"]
F -->|http| J["URL に POST"]
G --> K["exit code と stdout で<br/>decision を返す"]
H --> K
I --> K
J --> K
K --> L["Claude Code が結果を反映<br/>(allow / deny / context追加など)"]ポイントは matcher の段です。matcher は「この hook イベントのうちどれを実際に発火させるか」を絞り込む照合条件で、例えば PreToolUse の matcher に Bash と書けば Bash ツール呼び出しのときだけ hook が走り、mcp__memory__.* と書けば memory MCP の全 tool 呼び出しで走ります。matcher の書き方を間違えると、hook は静かに発火しないままになります。
matcher の型は hook ごとに違う
matcher は hook イベントごとに対応する値の型が違います。主要な hook について、公式 docs の Per-Hook Matcher Reference から抜粋します。
| イベント | matcher の対象 | 値の例 |
|---|---|---|
PreToolUse | tool_name | Bash / Edit / Write / mcp__memory__.* |
PostToolUse | tool_name | 同上 |
PermissionRequest | tool_name | 同上 |
Notification | notification_type | permission_prompt / idle_prompt / auth_success |
SubagentStart / SubagentStop | agent_type | Bash / Explore / Plan / カスタム名 |
SessionStart | source | startup / resume / clear / compact |
SessionEnd | reason | clear / resume / logout |
PreCompact / PostCompact | compact_trigger | manual / auto |
FileChanged | filename(basename) | .envrc|.env|.env.local |
UserPromptSubmit / Stop / TeammateIdle | — | matcher 非対応、常に発火 |
PreToolUse の matcher は正規表現対応で、mcp__memory__.*(memory MCP の全 tool)や mcp__.*__write.*(任意 MCP の write 系 tool)のような書き方ができます。MCP 連携は #10 Claude Code MCP 入門 で扱った mcp__<server>__<tool> 命名規則がそのまま使えます。
active な hook と matcher を確認するには、Claude Code 内で /hooks メニューを開きます。[User] / [Project] / [Local] / [Plugin] のラベルで、どの階層の hook が登録されているかが一覧できます。
踏みやすい罠 4 つ
罠 1: 重い処理を同期 hook に入れて Claude Code が固まる
type: "command" の hook は、デフォルトでは同期実行で、timeout まで Claude Code 本体をブロックします。lint・format・通知音のような「結果を後で見ればよい」処理に async: true を付け忘れると、毎回のツール呼び出しが遅くなります。
This project uses
async: truefor all hooks since sound notifications are side-effects that don’t need to block execution.このプロジェクトでは、サウンド通知は実行をブロックする必要のない副作用であるため、すべての hook で
async: trueを使用しています。
例えば HOOKS-README の運用も同じで、lint や通知音は async 一択です。決断を返したい hook(PreToolUse で deny したい等)だけ同期にします。
罠 2: matcher を間違えて hook が一度も発火しない
「PreToolUse の matcher に * と書いたら全 tool に発火すると思ったら、何も起きなかった」。* だけでは正規表現として「直前文字の 0 回以上」を意味するため、空文字列にしかマッチしません。全 tool を対象にしたいなら ".*" か、matcher 自体を省略します。
似た失敗で多いのは、SessionStart の matcher に Bash を入れてしまうケースです。SessionStart の matcher 対象は source で、値は startup / resume / clear / compact のいずれか。tool_name ではないので、Bash と書いても永遠に発火しません。
確認手順は次の通りです。
/hooksメニューで対象 hook が登録済みか確認- 公式 docs の Per-Hook Matcher Reference で、その hook の matcher 対象を確認
- 試しに matcher 自体を外してみて、hook そのものは正しく書けているか分離テスト
罠 3: 設定の置き場所を間違えてチーム全員に効いてしまう / 自分に効かない
「自分の作業用 hook をうっかり .claude/settings.json に commit してしまい、チーム全員の作業ログが汚れた」「逆に、チームに渡したい lint hook を .claude/settings.local.json に書いて、CI でだけ効かなくて詰んだ」。シリーズ #09 設定階層を理解すれば事故らない で扱った 5 層スタックがそのまま hook にも効きます。
- チーム共有 hook →
.claude/settings.json(commit 対象) - 自分専用 hook →
.claude/settings.local.json(.gitignoreで除外、commit しない) - 全プロジェクト共通 hook →
~/.claude/settings.json
claude-code-best-practice では、もう一段細かく .claude/hooks/config/hooks-config.json(共有)と .claude/hooks/config/hooks-config.local.json(個人)でオン・オフを切り替える運用が組まれていますが、これは個人と team の責任分界を強くしたい場合の追加レイヤーです。最初は settings.json の 2 層だけで十分です。
罠 4: PreToolUse の decision control を旧 API で書く
「古い Blog 記事を見て {"decision": "block"} を返したら効かない」。PreToolUse の decision control は、現行(v2.1 系)では hookSpecificOutput.permissionDecision を返す形に変わっています。公式から原文どおり引用します。
The
PreToolUsehook previously used top-leveldecisionandreasonfields for blocking tool calls. These are now deprecated. UsehookSpecificOutput.permissionDecisionandhookSpecificOutput.permissionDecisionReasoninstead.
PreToolUsehook は以前、ツール呼び出しをブロックするためにトップレベルのdecisionおよびreasonフィールドを使用していました。これらは現在非推奨です。代わりにhookSpecificOutput.permissionDecisionおよびhookSpecificOutput.permissionDecisionReasonを使用してください。
旧と現行の対応はこうです。
| 旧(deprecated) | 現行 |
|---|---|
{"decision": "approve"} | {"hookSpecificOutput": {"permissionDecision": "allow"}} |
{"decision": "block"} | {"hookSpecificOutput": {"permissionDecision": "deny"}} |
「動かない」と感じたら、まず最新の公式 reference を見直します。
今週やる 1 つ ─ commit 前 lint の PreToolUse を 15 行で書く
最初の hook は 「commit 前に lint をかける PreToolUse hook」 が現実的です。Bash tool で git commit が呼ばれる直前に npm run lint を走らせ、エラーなら commit を block します。.claude/settings.json に追記する形で書けば、チームで共有できます。
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "npm run lint",
"timeout": 30000,
"if": "Bash(git commit*)"
}]
}]
}
}
ポイントは if フィールド(v2.1.85 以降、HOOKS-README に記載)です。
The
iffield adds conditional execution to hooks using permission rule syntax. When set, the hook process is only spawned if the condition matches — reducing unnecessary process spawning.
ifフィールドは permission rule 構文を使った条件付き実行を hook に追加します。設定されている場合、条件が一致するときだけ hook プロセスが起動されるため、不必要なプロセス起動を削減できます。
if: "Bash(git commit*)" は「Bash tool で git commit から始まるコマンドが呼ばれたとき」だけ hook プロセスを起動します。ls や cat のような毎回の Bash 呼び出しでは一切 npm を立ち上げないので、CPU と時間の両方を節約できます。
npm run lint が exit code 0 以外で終了すると、Claude Code はそのツール呼び出しを失敗扱いにし、commit を続けるか中止するかの判断を Claude 側に促します。lint エラーで commit が止まる体験を 1 度すれば、「先に lint かけて」と毎回プロンプトに書く生活には戻れません。
まとめ
Hooks は Claude Code を「言われたことしかやらない助手」から「ライフサイクルの特定地点で勝手に動く相棒」に変える仕組みです。押さえる軸は次の 4 つです。
- イベント: 29 種類を 3 つの発火頻度(セッションあたり / ターンあたり / ツール呼び出しあたり)で整理
- handler type: command / prompt / agent / http の 4 種類。日常は command
- matcher: hook ごとに対応する値の型が違う。
/hooksメニューで確認 - decision control: PreToolUse の旧
decisionフィールドは deprecated。現行はhookSpecificOutput.permissionDecision
シリーズ #09 .claude/settings.json の 5 階層 と組み合わせれば、「チームに効かせたい lint hook は .claude/settings.json、自分専用の通知音は .claude/settings.local.json」のような階層運用が組めます。
パイソンエンジニア部 

