原典: 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 も決めず他に答えるものも無い呼び出しは拒否される
PermissionDeniedauto mode が拒否したとき(分類器の判定を伴わない拒否も含む)。hookSpecificOutput.retry: true でモデルに再試行を許可できるが、分類器が判定を出していないときは retry が無視される
PostToolUseツール呼び出しの成功後
PostToolUseFailureツール呼び出しの失敗後
PostToolBatch並列ツール呼び出しのバッチ全体が解決した後、次のモデル呼び出しの前
Notification通知が送られるとき
MessageDisplayassistant のメッセージテキストが表示される間
SubagentStart / SubagentStopsubagent の起動 / 終了
TaskCreated / TaskCompletedTaskCreate でタスクが作られるとき / 完了マークが付くとき
StopClaude が応答を終えたとき。タスク完了時だけではない。ユーザーの中断では発火しない
StopFailureAPI エラーでターンが終わったとき(このとき Stop は発火しない)。matcher でエラー種別を絞れる —— rate_limit / overloaded / max_output_tokens など
TeammateIdleagent team の teammate が idle になろうとするとき
InstructionsLoadedCLAUDE.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 / PostCompactcontext compaction の前 / 後
PreModelSwitchモデルの切り替えを適用する前。ブロックできる
PostModelSwitchセッションのモデルが変わった後。Claude Code 自身による変更も含む(セッション再開時にモデルを戻すときなど)
Elicitation / ElicitationResultMCP サーバがツール呼び出し中にユーザー入力を要求したとき / 応答後
SessionEndセッション終了時

hook の5つの型

type内容
command(既定)シェルコマンドを実行する
httpURL にイベントデータを 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 additionalContext inside hookSpecificOutput; 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 deny to suppress side effects in another hook.

ある hook の deny が、別の hook の副作用を抑えてくれることを当てにするな。

  • PreToolUse の権限判断は最も制限的なものが勝つ。順序は deny > defer > ask > allow
  • additionalContext は全 hook の分が保持されてまとめて Claude に渡る

matcher と if

  • matcher — グループ単位でツール名だけで絞る。パイプ | で複数指定できる(Edit|Write。カンマ区切り Edit, Write も同じ意味)
  • if — permission rule 構文でツール名と引数を合わせて絞る。マッチしたときだけ hook プロセスが起動する

if の挙動(複合コマンドの扱いが重要):

if パターンBash コマンド走るか理由
Bash(git *)git pushyesコマンド名が一致
Bash(git *)npm test && git pushyes各サブコマンドが検査される
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 / PostCompactcompaction のきっかけmanual / auto
SubagentStart / SubagentStopagent の型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 / ElicitationResultMCP サーバ名設定済みのサーバ名
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.jsonplugin が有効な間可
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 hookhook の入力データだけで判断できるとき
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)。

効かないときに見るところ

  • Stop hook が、その間に 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 を探す
  • 複数の PreToolUse hook が 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 0

exit 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)