原典: Automate actions with hooks / 日本語版
一行要約
hook の価値は「確実に起きる」ことにある — LLM が実行を選ぶかどうかに依存しない決定的な制御であり、判断が要る場面のために prompt / agent 型(LLM を挟む hook)まで用意されている。
要点
Hooks are user-defined shell commands. Claude Code runs them at specific points in its lifecycle, which gives you deterministic control: certain actions always happen rather than relying on the LLM to choose to run them.
hook はユーザーが定義するシェルコマンドである。Claude Code はそれを自身のライフサイクルの特定の時点で実行する。これにより決定的な制御が得られる。LLM が実行を選ぶことに頼るのではなく、特定の動作が必ず起きる。
lifecycle イベント一覧
| イベント | 発火タイミング |
|---|---|
SessionStart | セッション開始 / 再開時 |
Setup | --init-only、または -p モードの --init / --maintenance。CI やスクリプトでの一度きりの準備用 |
UserPromptSubmit | プロンプトが送られたとき、Claude が処理する前。Claude Code が自分で始めるターンでも発火する(hooks) |
UserPromptExpansion | ユーザーが打ったコマンドがプロンプトに展開されるとき。展開をブロックできる |
PreToolUse | ツール呼び出しの実行前。ブロックできる |
PermissionRequest | ツール呼び出しに権限判断が必要になったとき、または確認を出せない呼び出しを自動で拒否しようとするとき。-p でも dontAsk 以外では走り、どの hook も決めず他に答えるものも無い呼び出しは拒否される |
PermissionDenied | auto mode が拒否したとき(分類器の判定を伴わない拒否も含む)。hookSpecificOutput.retry: true でモデルに再試行を許可できるが、分類器が判定を出していないときは retry が無視される |
PostToolUse | ツール呼び出しの成功後 |
PostToolUseFailure | ツール呼び出しの失敗後 |
PostToolBatch | 並列ツール呼び出しのバッチ全体が解決した後、次のモデル呼び出しの前 |
Notification | 通知が送られるとき |
MessageDisplay | assistant のメッセージテキストが表示される間 |
SubagentStart / SubagentStop | subagent の起動 / 終了 |
TaskCreated / TaskCompleted | TaskCreate でタスクが作られるとき / 完了マークが付くとき |
Stop | Claude が応答を終えたとき。タスク完了時だけではない。ユーザーの中断では発火しない |
StopFailure | API エラーでターンが終わったとき(このとき Stop は発火しない)。matcher でエラー種別を絞れる —— rate_limit / overloaded / max_output_tokens など |
TeammateIdle | agent team の teammate が idle になろうとするとき |
InstructionsLoaded | CLAUDE.md や .claude/rules/*.md が context にロードされたとき。セッション開始時だけでなく、セッション中に遅延ロードされたときも発火する |
ConfigChange | セッション中に設定ファイルが変わったとき |
CwdChanged | 作業ディレクトリが変わったとき(Claude が cd を実行したときなど)。direnv のような反応的な環境管理に有用 |
DirectoryAdded | /add-dir または SDK の register_repo_root 制御リクエストで、セッション途中に作業ディレクトリが追加されたとき |
FileChanged | 監視対象ファイルがディスク上で変更されたとき。matcher で監視対象のファイル名を指定する |
WorktreeCreate | --worktree、isolation: "worktree"、またはバックグラウンドセッションのために worktree が作られるとき。既定の git 挙動を置き換える |
WorktreeRemove | セッション終了時・subagent の終了時・バックグラウンドセッションの削除時に worktree が消されるとき |
PreCompact / PostCompact | context compaction の前 / 後 |
PreModelSwitch | モデルの切り替えを適用する前。ブロックできる |
PostModelSwitch | セッションのモデルが変わった後。Claude Code 自身による変更も含む(セッション再開時にモデルを戻すときなど) |
Elicitation / ElicitationResult | MCP サーバがツール呼び出し中にユーザー入力を要求したとき / 応答後 |
SessionEnd | セッション終了時 |
hook の5つの型
type | 内容 |
|---|---|
command(既定) | シェルコマンドを実行する |
http | URL にイベントデータを POST する |
mcp_tool | 設定済みの MCP サーバのツールを呼ぶ |
prompt | 単発の LLM 評価(model で変更可。既定は Claude Code がバックグラウンド処理に使うモデル —— hooks) |
agent | ツールを使える多ターンの検証(experimental) |
入出力と exit code
hook は stdin / stdout / stderr と exit code で Claude Code と通信する。イベント固有のデータが JSON で stdin に渡る(共通フィールドは session_id と cwd)。
| exit code | 挙動 |
|---|---|
| 0 | 異議なし。PreToolUse では承認ではない — 通常の権限フローがそのまま適用される。UserPromptSubmit / UserPromptExpansion / SessionStart / PostModelSwitch では stdout が Claude の context に追加される |
| 2 | ブロックする。 stderr に理由を書く。行き先はイベントによって違う —— Claude にフィードバックとして渡るもの、ユーザーに表示されるもの、ConfigChange や Elicitation のように何も出ないものがある。ブロックできないイベントもある(SessionStart など。stderr はユーザーに表示され実行は続く) |
| その他 | stdout に何を出したかで決まる。 schema 検証を通る JSON オブジェクトならexit code のほうが無視され、JSON が結果を決める(エラー扱いにもならない。WorktreeCreate のように nonzero で失敗するイベント例外はある)。JSON として壊れている/schema に落ちるなら非ブロックのエラー。ただの文字列や空なら実行は続き、transcript に <hook name> hook error と stderr の1行目が Failed with non-blocking status code: 付きで出る。全文は claude --debug か /debug で見る |
構造化 JSON 出力(exit 0 で stdout に JSON を出す)を使うともっと制御できる。
Use exit 2 to block with a stderr message, or exit 0 with JSON for structured control. Choose one approach per hook.
ブロックするには、終了コード 2 と stderr のメッセージを使うか、終了コード 0 と JSON による構造化された制御を使う。hook ごとにどちらか一方を選べ。
PreToolUse の permissionDecision の値:
"allow"— 対話的な権限プロンプトをスキップする。ただし deny / ask ルール(企業の managed deny list を含む)は依然として適用される。MCP ツールのrequiresUserInteractionや、組織がaskにしたコネクタツールのプロンプトも消せない"deny"— ツール呼び出しをキャンセルし、理由を Claude に送る"ask"— 通常どおりユーザーに確認する"defer"— 非対話モード(-p)でのみ。ツール呼び出しを保持したままプロセスを終了し、Agent SDK のラッパーが入力を集めて再開できるようにする
PreModelSwitch も同じ permissionDecision を返す —— "allow" で切り替えを通し、"deny" で取り消す。"ask" が確認になるのは対話セッションで /model を打ったときだけで、それ以外の場所では拒否として扱われる。
イベントによって判断の書き方が違う。 PostToolUse と Stop はトップレベルの decision: "block"、PermissionRequest は hookSpecificOutput.decision.behavior を使う。
UserPromptSubmit では hookSpecificOutput.additionalContext で Claude の context にテキストを注入する。
Nest
additionalContextinsidehookSpecificOutput; if you place it at the top level of the JSON, Claude Code silently ignores it.
additionalContextはhookSpecificOutputの内側に入れよ。JSON の最上位に置くと、Claude Code はそれを黙って無視する。
複数の hook が同じイベントにマッチしたとき
- すべての hook が並列に走り、全部が完了してから結果がマージされる
- 1つが
denyを返しても、兄弟の hook の実行は止まらない
Don’t rely on one hook’s
denyto suppress side effects in another hook.ある hook の
denyが、別の hook の副作用を抑えてくれることを当てにするな。
PreToolUseの権限判断は最も制限的なものが勝つ。順序はdeny>defer>ask>allowadditionalContextは全 hook の分が保持されてまとめて Claude に渡る
matcher と if
matcher— グループ単位でツール名だけで絞る。パイプ|で複数指定できる(Edit|Write。カンマ区切りEdit, Writeも同じ意味)if— permission rule 構文でツール名と引数を合わせて絞る。マッチしたときだけ hook プロセスが起動する
if の挙動(複合コマンドの扱いが重要):
if パターン | Bash コマンド | 走るか | 理由 |
|---|---|---|---|
Bash(git *) | git push | yes | コマンド名が一致 |
Bash(git *) | npm test && git push | yes | 各サブコマンドが検査される |
Bash(git *) | echo $(git log) | yes | $() やバッククォート内も検査される |
Bash(git *) | echo $(date) | no | どのサブコマンドも一致しない |
Bash(git push *) | echo $(date) | yes | コマンド名以上を指定したパターンは、$() / バッククォート / $VAR があると念のため走る |
When Claude Code can’t determine which commands the Bash input runs, it runs your hook regardless of the pattern. The Bash matching table covers the command shapes Claude Code can and can’t narrow by subcommand. Because the filter is best-effort, use the permission system rather than a hook to enforce a hard allow or deny.
Bash の入力がどのコマンドを走らせるか判断できないとき、Claude Code はパターンによらず hook を実行する。どの形のコマンドなら副コマンド単位まで絞れるかは、上の表と hooks の Bash マッチングの表にある。フィルタは best-effort なので、厳格な許可・拒否を強制したいなら、hook ではなく permission の仕組みを使え。
if はツールイベントでのみ機能する(PreToolUse、PostToolUse、PostToolUseFailure、PermissionRequest、PermissionDenied)。他のイベントに付けると hook が走らなくなる。
matcher がツール名を絞るのはツールイベントだけで、他のイベントでは別のものを絞る。
| イベント | matcher が絞るもの | 値の例 |
|---|---|---|
SessionStart | 始まり方 | startup / resume / clear / compact / fork |
SessionEnd | 終わり方 | clear / resume / logout / prompt_input_exit / other |
Setup | どの CLI フラグで起きたか | init / maintenance |
PreCompact / PostCompact | compaction のきっかけ | manual / auto |
SubagentStart / SubagentStop | agent の型 | general-purpose / Explore / Plan / 自作 agent 名 |
ConfigChange | どの設定ファイルか | user_settings / project_settings / local_settings / policy_settings / skills |
InstructionsLoaded | ロードされた理由 | session_start / nested_traversal / path_glob_match / include / compact |
PreModelSwitch / PostModelSwitch | 切り替え先モデルの正式名 | claude-opus-5 / .*opus.* |
Notification | 通知の種別 | permission_prompt / idle_prompt / auth_success / agent_needs_input / agent_completed など |
DirectoryAdded | どう追加されたか | slash_command / register_repo_root |
FileChanged | 監視するファイル名そのもの(正規表現ではなくリテラルとして | で分割される) | .envrc|.env |
UserPromptExpansion | コマンド名 | skill / コマンドの名前 |
Elicitation / ElicitationResult | MCP サーバ名 | 設定済みのサーバ名 |
| matcher 非対応 | UserPromptSubmit、PostToolBatch、Stop、TeammateIdle、TaskCreated、TaskCompleted、WorktreeCreate、WorktreeRemove、CwdChanged、MessageDisplay | 毎回発火する |
置き場所とスコープ
| 場所 | スコープ | 共有 |
|---|---|---|
~/.claude/settings.json | 全プロジェクト | 不可(マシンローカル) |
.claude/settings.json | 単一プロジェクト | 可(リポジトリにコミットできる) |
.claude/settings.local.json | 単一プロジェクト | 不可(gitignore される) |
| managed policy settings | 組織全体 | 可(管理者が制御) |
plugin の hooks/hooks.json | plugin が有効な間 | 可 |
| skill の frontmatter | その skill を呼んだ後、セッションの残り全体(skill が終わっても戻らない) | 可 |
| subagent の frontmatter | その subagent が走っている間 | 可 |
/hooksでイベント別に一覧できる(読み取り専用)。Only hooks from managed settings run hereと出たら組織がallowManagedHooksOnlyを設定している —— user / project / local の設定ファイルの hook は走らず、一覧にも出ない"disableAllHooks": trueで無効化できる。読まれるのは設定の優先順位を解決した後の値なので、プロジェクトの設定が自分の設定を上書きしうる。managed settings の hook は、そちらにも同じ設定を入れない限り走り続ける- Claude Code の実行中に設定ファイルを直接編集しても、file watcher が変更を拾う
prompt hook と agent hook の使い分け
どちらも {"ok": bool, "reason": string} を返す。
"ok": true— 続行"ok": false— イベントによって挙動が変わるStop/SubagentStop—reasonが Claude に戻され、作業を続けさせる。ただし"impossible": trueを併せて返すと「どうやっても満たせない条件」として扱われ、停止が許されてターンが終わるPreToolUse— ツール呼び出しが拒否される。既定ではターンが終わり、警告行として表示される。continueOnBlock: trueを設定するとreasonがツールエラーとして Claude に返り、続行できる(v2.1.210 より前は、こちらが既定の挙動だった)PostToolUse— 既定でターンが終わる。continueOnBlock: trueで続行PostToolBatch/UserPromptSubmit/UserPromptExpansion— ターンが終わり、reasonが警告行として出る
| 使うとき | |
|---|---|
| prompt hook | hook の入力データだけで判断できるとき |
| agent hook | コードベースの実際の状態に照らして検証する必要があるとき。ファイルを読み、コマンドを走らせられる。既定タイムアウト 60秒、最大50ツールターン |
agent hook では $ARGUMENTS が hook の JSON 入力に置換される。agent hook に impossible は無く、continueOnBlock フィールドも持たない —— ok: false は常に continueOnBlock: true の prompt hook と同じ扱いになるので、PreToolUse / PostToolUse ではターンが続く。
タイムアウト
既定値は型とイベントで違い、timeout(秒)で hook ごとに上書きできる。 値の一覧は hooks の「ハンドラ種別ごとのフィールド」にある(SessionEnd だけは hook ごとではなく全体で予算を共有する点に注意)。
permission mode との関係
PreToolUse hook は permission mode の判定より前に、すべての mode で発火する(dontAsk でも)。permissionDecision: "deny" を返す hook は、bypassPermissions モードでも --dangerously-skip-permissions でもツールを止める。ユーザーが mode を変えても回避できない方針を強制できる。
逆は成り立たない。 "allow" は設定の deny ルールを越えられず、requiresUserInteraction の MCP ツールや組織が ask にしたコネクタツールのプロンプトも消せない。設定ファイルと plugin の hooks/hooks.json の hook は制限を締める方向にしか効かない。 ただし tool.check を扱う mod は、PreToolUse hook がブロックした呼び出しを承認できる(hook が managed settings にあれば別。どのルールが mod に勝つかは permissions)。
効かないときに見るところ
Stophook が、その間に Claude のツール呼び出しを挟まずに8回連続でブロックすると、Claude Code が hook を上書きしてターンを終わらせる。 入力 JSON のstop_hook_activeを読み、trueなら早期に exit 0 する。正当に8回以上要るならCLAUDE_CODE_STOP_HOOK_BLOCK_CAPで上限を上げる<hook name> hook errorに JSON のパースエラーが出る —— stdout が JSON オブジェクトに見えたが正しい JSON ではなかった(exit 0 でも起きる)。文字列連結ではなくjqのような JSON エンコーダで組み立てる(値の中の引用符やバックスラッシュがエスケープされるように)- JSON を出したのに何も起きず、エラーも出ない —— 疑うのは2つ
- JSON の前に別の出力が混ざっている。 shell 形式(
args無し)の hook は非対話シェルで走るが、Git Bash やBASH_ENVの設定ではプロファイルが読まれる。プロファイルの無条件echoが先に出ると stdout が{で始まらなくなり、全体がただの文字列として扱われて JSON が無視される。プロファイルのechoはif [[ $- == *i* ]]で対話シェルに限る - フィールドの階層が違う。
permissionDecisionやadditionalContextをトップレベルに置くと JSON 自体は通るので、エラーを出さずに黙って無視される。claude --debugのログでHook JSON output had unrecognized keysを探す
- JSON の前に別の出力が混ざっている。 shell 形式(
- 複数の
PreToolUsehook がupdatedInputで引数を書き換えると、最後に終わったものが勝つ。 並列実行なので順序は不定 —— 同じツールの入力を2つ以上の hook で書き換えない
そのまま使える具体例
編集のたびに Prettier を走らせる(.claude/settings.json):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}保護ファイルの編集をブロックする(.claude/hooks/protect-files.sh):
#!/bin/bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Normalize Windows backslash separators so the patterns below match
FILE_PATH="${FILE_PATH//\\//}"
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0exit 2 でブロックする最小形:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q "drop table"; then
echo "Blocked: dropping tables is not allowed" >&2 # stderr becomes Claude's feedback
exit 2 # exit 2 = block the action
fi
exit 0 # exit 0 = no decision; the normal permission flow applies構造化 JSON で拒否して理由を伝える:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}毎プロンプトに context を注入する:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Current branch: release-42. Deploy freeze until Friday."
}
}ログ用と guardrail 用の2つを同じイベントに登録する(両方走り、deny が勝ち、ログは残る):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r .tool_input.command >> ~/.claude/bash.log"
},
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
}
]
}
]
}
}if で git コマンドのときだけ走らせる:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}
]
}
]
}
}prompt hook で「本当に終わったか」を判定させる:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
}
]
}
]
}
}agent hook でテストの通過を検証させる:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
"timeout": 120
}
]
}
]
}
}PermissionRequest で決まりきった承認を飛ばす(例は ExitPlanMode。matcher は必ず絞る —— 空や .* にするとファイル書き込みもシェルコマンドも全部自動承認になる):
{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
}
]
}
]
}
}承認すると transcript にはダイアログの代わりに “Allowed by PermissionRequest hook” が出る。hook 経由の承認は必ず今の会話を引き継ぐ —— ダイアログのように context を捨てて実装セッションを新しく始めることはできない。decision に updatedPermissions の setMode を入れると mode も変えられる(destination: "session" でそのセッション限り)。bypassPermissions にできるのは、そもそも bypass が使える状態で起動したときだけ(--dangerously-skip-permissions、--permission-mode bypassPermissions、--allow-dangerously-skip-permissions、または user / --settings / managed settings の permissions.defaultMode: "bypassPermissions")。disableBypassPermissionsMode で無効化されているときと、restricted mode(--restricted)で起動したときは効かない。 Claude Code がそれを defaultMode として保存することはない。
ディレクトリごとの環境変数を Bash ツールにも効かせる(CLAUDE_ENV_FILE は Bash コマンドの前に走るスクリプトの前置き。SessionStart で起動時のぶんを読み、CwdChanged で cd のたびに読み直す):
{
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "direnv export bash > \"$CLAUDE_ENV_FILE\"" }] }
],
"CwdChanged": [
{ "hooks": [{ "type": "command", "command": "direnv export bash > \"$CLAUDE_ENV_FILE\"" }] }
]
}
}ディレクトリの変化ではなく特定のファイルの変化に反応させるなら FileChanged に監視するファイル名を | 区切りで書く(".envrc|.env")。
設定ファイルの書き換えを監査する(ConfigChange は外部のエディタやプロセスが設定を変えたときに発火する。exit 2 か {"decision": "block"} で変更を止めることもできる):
{
"hooks": {
"ConfigChange": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c '{timestamp: now | todate, source: .source, file: .file_path}' >> ~/claude-config-audit.log"
}
]
}
]
}
}macOS のデスクトップ通知(~/.claude/settings.json):
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}HTTP hook(allowedEnvVars に載せた変数だけが展開される):
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/tool-use",
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}
]
}
]
}
}原典で言及されている関連文書
- features-overview — 「強制したいことは hook にする」の判断ガイド
- memory — CLAUDE.md は指示であって強制ではない(だから hook が要る)
- permission-modes — hook と permission の関係
- skills — skill frontmatter の
hooks - agent-teams —
TeammateIdle/TaskCreated/TaskCompleted - hooks — 完全なイベントスキーマ、非同期 hook、MCP tool hook
- permissions —
ifが使う permission rule 構文。厳格な許可・拒否はこちらで書く - headless —
-pで確認に答えるホストと、無人実行での拒否 - env-vars —
CLAUDE_CODE_STOP_HOOK_BLOCK_CAP - settings —
disableAllHooksの効く範囲 - mcp —
requiresUserInteractionとコネクタツールの組織設定 - sub-agents — subagent frontmatter の
hooks - plugins — plugin の
hooks/hooks.json(書き方と移行時の二重実行は plugins-create)