Claude Code hooksの仕組みと設定方法
Claude Code hooksはモデルの同意なしに実行されます。設定場所、発火するイベント、exit 2でツール呼び出しをキャンセルする仕様、セキュリティ上の注意点をrelease 2.1.232で確認します。
Claude Code の hook とは
Claude Code の hook は、Claude Code が自身のライフサイクル上の決められた時点で自動的に実行するシェルコマンドです。これが hook と rules ファイルの根本的な違いです。CLAUDE.md の指示は助言であり、モデルはコンテキスト内の他の情報と比較して判断します。hook はコードであり、モデルが同意するかどうかに関係なく実行されます。フォーマッターについて 2 回指示してもエージェントが実行を省略し続けるなら、より強い指示は必要ありません。hook が必要です。
仕組みは小規模です。イベント名を指定して、設定ファイルにコマンドを登録します。そのイベントが発生すると、Claude Code はコマンドを実行し、イベントデータを JSON(JavaScript object notation)として標準入力(stdin)に書き込みます。コマンドはそのデータを読み取り、処理を実行し、終了ステータスを返します。PreToolUse hook が exit 2 を返すと、ツール呼び出しは実行前にキャンセルされます。スクリプトが標準エラー出力(stderr)に書き込んだ内容は、理由としてモデルに渡されます。
ここで使用しているイベント名とフィールド名は、Claude Code の hooks リファレンスに基づいています。2026 年 8 月に、release 2.1.232 を対象として確認しています。この仕様は頻繁に変更されるため、この文書を含むブログ記事から JSON をコピーする前に、使用しているバージョンのリファレンスを確認してください。claude --version で自分のバージョンを出力できます。
フック設定の保存場所
フックは、設定ファイル内の JSON ブロックです。6 か所に定義でき、フックのスコープはそのファイルのスコープになります。
~/.claude/settings.json: 自分のマシン上のすべてのプロジェクトに適用されます。ほかのユーザーには適用されません。.claude/settings.json: 1 つのプロジェクトに適用され、リポジトリにコミットされます。そのため、クローンした全員がフックを取得します。.claude/settings.local.json: 1 つのプロジェクトに適用されます。自分のマシンにだけ存在します。- 管理ポリシー設定: 管理者が設定し、組織全体に適用されます。
- プラグイン内の
hooks/hooks.json: そのプラグインが有効な間だけ有効です。 - スキルまたはサブエージェントの frontmatter: そのコンポーネントが有効な間だけ有効です。
これらのファイルにあるフックエントリは、互いに上書きされず、マージされます。プロジェクト設定ファイルにフックを追加すると、ユーザー設定のフックが置き換わるのではなく、そこに追加されます。そのため、1 つのイベントに複数のファイルのフックを登録できます。"disableAllHooks": true を設定するとフックは無効になります。ただし例外があります。管理ポリシー設定のフックは、その設定も管理設定で適用しない限り、実行され続けます。
セッション内で /hooks を実行すると、現在登録されているすべてのフックがイベント別に一覧表示されます。各フックのソースファイルと matcher も表示されます。このメニューは読み取り専用です。フックを変更するには、設定ファイルを編集します。通常はファイル監視機能が編集を検知するため、再起動は必要ありません。
Claude Code に存在する hook イベント
Release 2.1.232 では、SessionStart から SessionEnd までの31個のイベントが定義されています。compaction、subagents、worktrees、configuration files などを扱います。サーバー運用で使用するのは、そのうち一部です。
PreToolUse: tool call の実行前です。実行をブロックできるのはこのイベントです。PostToolUse: tool call が成功した後です。失敗した場合はPostToolUseFailureが発生します。そのため、すべての結果を取得する hook には両方が必要です。PermissionRequest: tool call に permission の判定が必要になったときです。approval prompt が表示されるタイミングに当たります。UserPromptSubmit: prompt を送信したときです。Claude が処理する前に実行されます。この hook が stdout に出力した内容は、model の context に追加されます。SessionStartとSessionEnd: session の各終了時です。SessionStartは compaction の後にも、matcher の値compactで発生します。Stop: Claude が応答を完了したときです。完了した task ごとではなく、turn ごとに1回発生します。
各グループには matcher があり、どの発生時に hook を実行するかを決めます。tool イベントでは tool name で絞り込むため、"Edit|Write" は file edits に対してだけ発生します。Matchers は大文字と小文字を区別します。空の matcher はすべての発生時に実行されます。MCP (model context protocol) server の tools は mcp__<server>__<tool> という名前になるため、"mcp__github__.*" の matcher で1つの server の tools だけを対象にし、他の server は除外できます。
Stop hooks には、作成前に知っておくべき注意点があります。実行をブロックする Stop hook は model を再処理に戻しますが、Claude Code は8回連続でブロックされると hook を上書きします。hook input の stop_hook_active field を読み取り、その値が true の場合は exit 0 を返してください。そうしないと、上限に達するまで hook がループします。
標準入力でフックが受け取る内容
Claude が npm test を実行しようとすると、Bash 上の PreToolUse フックは標準入力から次の内容を読み取ります。
{
"session_id": "abc123",
"cwd": "/home/deploy/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}すべてのイベントには session_id、cwd、permission_mode、transcript_path、hook_event_name が含まれます。ツールイベントには tool_name、tool_input、tool_use_id も追加されます。その他のイベントには、それぞれ固有のフィールドがあります。UserPromptSubmit には prompt のテキストが入り、SessionStart には startup の source、resume、clear、compact、fork のいずれかが入ります。
シェルスクリプト内でこれを読み取るには、通常 jq を使用します。最小構成のサーバーイメージにはこれが含まれていないため、Ubuntu と Debian ではまず sudo apt install -y jq でインストールします。
ツール呼び出し中の終了ステータスが与える影響
結果は3通りです。
- Exit 0 は、フックが異議を申し立てなかったことを示します。
PreToolUseでは、これは承認と同じではなく、通常の権限確認フローが引き続き実行されます。UserPromptSubmitとSessionStartでは、標準出力がモデルのコンテキストに追加されます。 - Exit 2 は、
PreToolUseなど、ブロック可能なイベントでアクションをブロックします。標準エラー出力が、モデルに表示される理由になります。PostToolUseのようにブロックできないイベントでは、ブロックは無視されます。ただし、標準エラー出力はフィードバックとしてモデルに送られます。 - その他の終了コード は、ブロックを伴わないエラーです。アクションは続行されます。トランスクリプトには、
Failed with non-blocking status code:というテキストの後に標準エラー出力の1行目を含むフックエラー通知が表示されます。
ブロックする、または何も出力しない以外の処理を行う場合は、Exit 0 を返し、代わりに標準出力へ JSON オブジェクトを出力します。PreToolUse フックでは、permissionDecision によって判断します。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database drops go through a migration, not through the agent."
}
}"allow" は対話プロンプトを省略し、"deny" は呼び出しをキャンセルして理由をモデルに送信し、"ask" は通常どおりプロンプトを表示します。各フックでは、どちらか一方の方式に統一してください。Exit 2 と標準出力の JSON 判定を混在させると、結果の確認が必要になります。
1つのイベントに複数のフックが一致した場合、それらは並列に実行され、すべて完了まで実行されます。1つのフックからの deny が、他のフックを停止させることはありません。そのため、ガードレール用フックが同じ呼び出しを拒否しても、ロギング用フックはその行を書き込みます。Claude Code はその後、各回答を統合し、deny、defer、ask、allow の順で最も制限の厳しい判定を採用します。
例1: 破壊的なコマンドの実行前にブロックする
これをプロジェクト内の .claude/hooks/block-destructive.sh として保存します。
#!/bin/bash
# Deny a Bash tool call whose command matches a banned pattern.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
for pattern in 'rm -rf /' 'mkfs' 'dd if=' 'DROP TABLE'; do
if printf '%s' "$COMMAND" | grep -qiF -- "$pattern"; then
echo "Blocked by policy: the command matches '$pattern'. A human runs this one." >&2
exit 2
fi
done
exit 0実行可能にしてから、.claude/settings.json の PreToolUse に登録します。
chmod +x .claude/hooks/block-destructive.sh{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh",
"timeout": 10,
"statusMessage": "Checking the command against policy"
}
]
}
]
}
}信頼する前に、スクリプトを手動でテストしてください。フックが自身の入力でクラッシュすると、フェイルオープンになるためです。
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /var/lib/postgresql"}}' \
| .claude/hooks/block-destructive.sh
echo $?stderr に Blocked by policy: の行が表示され、終了コードが 2 になることを確認します。ls -la のような無害なコマンドを渡すと、出力はなく、終了コードは 0 になります。セッションでは、拒否された呼び出しが、理由として指定したメッセージとともにトランスクリプトに表示されます。モデルはそのメッセージを読み取り、動作を調整します。
これを実施する価値がある理由は、PreToolUse フックが、すべての権限モードで、権限モードのチェックより前に実行されることです。そのため、bypassPermissions の場合でも deny が有効になります。権限確認を減らしつつフックを実行できる Claude Code の自動モードと権限設定 と組み合わせることで、フックが有効になります。
この仕組みの限界も理解しておく必要があります。コマンド文字列のパターンマッチングは、エージェントの不注意な操作に対するガードレールです。しかし、巧妙なエージェントに対する境界にはなりません。同じコマンドでも、grep が認識できない形式で記述できるためです。強制すべきルールは、権限システムと、プロセスを実行するアカウントに設定してください。
例 2: 編集のたびにフォーマットと lint を実行する
PostToolUse と Edit|Write の matcher を組み合わせると、ファイル編集ツールの実行後に処理を実行できます。次の内容を .claude/hooks/after-edit.sh として保存します。
#!/bin/bash
# Format the edited file, then report lint failures back to the model.
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
case "$FILE" in
*.py)
ruff format "$FILE" >/dev/null 2>&1
if ! ruff check "$FILE" >&2; then
exit 2
fi
;;
*.sh)
if ! shellcheck "$FILE" >&2; then
exit 2
fi
;;
esac
exit 0{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/after-edit.sh",
"timeout": 60
}
]
}
]
}
}Claude に Python ファイルへインデントの崩れた関数を追加するよう依頼し、その後ファイルを開きます。ファイルがフォーマット済みの状態で返れば、hook が実行されたことを確認できます。hook が正常に実行された場合、会話には何も表示されないためです。
ここでの exit 2 は、変更を取り消しません。PostToolUse はツールの実行後に発火するため、どちらの場合でも編集内容はディスクに保存されています。exit 2 によって、ruff check の出力をフィードバックとしてモデルに渡せます。そのため、モデルは直前に導入したエラーを修正してから先に進みます。これは、commit 時に初めて見つかる lint 失敗と、エージェントが同じターン内で修正する lint 失敗の違いです。
ここでは、2 つの matcher の制限が重要です。Edit|Write は shell command によって変更されたファイルを認識しません。また、Claude は十分な頻度で Bash を通じてファイルを書き込むため、この差は無視できません。呼び出しごとの適用範囲を確保するには、Bash も matcher に含め、スクリプトで変更されたファイルを git status --porcelain により一覧表示します。ターンごとに 1 回だけ適用する場合は、代わりにスキャン処理を Stop hook に配置します。
例 3: 監査用にすべてのツール呼び出しをログに記録する
PostToolUse に空の matcher を指定すると、すべてのツールで実行されます。記録をホームディレクトリ内のファイルではなく system journal に送ることで、エージェント自身の shell からアクセスできない場所に保管できます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c '{time: now|todate, session: .session_id, cwd: .cwd, tool: .tool_name, input: .tool_input}' | logger -t claude-code -p local0.info"
}
]
}
]
}
}journalctl -t claude-code -o cat | tail -n 5 でログを読み戻します。ツール呼び出しごとに 1 行の JSON が表示され、最後の行が最新の記録になっているはずです。何も表示されない場合は hook が実行されていません。下記のトラブルシューティングのセクションで対処方法を説明します。
失敗した呼び出しも記録するには、PostToolUseFailure の下に同じブロックを追加します。PostToolUse は成功時にしか実行されず、通常は失敗したコマンドのほうが重要だからです。ホームディレクトリ内のファイルへの追記ではなく logger を使用する理由は、所有権にあります。hook はエージェントの shell と同じユーザーとして実行されるため、そのユーザーが追記できるものは、そのユーザー自身が切り詰めることもできます。journal は systemd-journald が専用のアカウントで書き込みます。
フックの実行時間
The data behind this chart
[
{
"label": "command, http or mcp_tool hook",
"default_timeout_seconds": 600
},
{
"label": "agent hook",
"default_timeout_seconds": 60
},
{
"label": "prompt hook",
"default_timeout_seconds": 30
},
{
"label": "command hook on UserPromptSubmit",
"default_timeout_seconds": 30
},
{
"label": "command hook on MessageDisplay",
"default_timeout_seconds": 10
},
{
"label": "any hook on SessionEnd",
"default_timeout_seconds": 1.5
}
]コマンドフックには、デフォルトで 600 秒、つまり 10 分の実行時間が与えられます。一部のイベントでは、この時間が大幅に短くなります。SessionEnd フックはすべて合わせて 1.5 秒の予算を共有するため、セッション終了時のクリーンアップは迅速に行う必要があります。ただし、フックでより長い timeout を設定すると、共有予算も同じ値まで増やせます。上限は 60 秒です。
タイムアウトに達したフックはキャンセルされ、判定を返しません。PreToolUse ガードレールの場合、これは処理をブロックしないことを意味します。ツール呼び出しは通常の権限フローに進みます。そのため、ガードレールのスクリプトは小さく保ってください。ログの別の場所への送信など、処理中に待機する必要がない時間のかかる処理では "async": true を設定します。フックはバックグラウンドで実行され、ツール呼び出しを遅延させません。
Hooks、ルールファイル、skills、MCP サーバー
これらはすべてエージェントの動作を変えるため、互いに混同されがちです。ただし、提案にとどまらないのはこのうち 1 つだけです。
ルールファイル(CLAUDE.md、または .claude/rules/ 配下のファイル)は、モデルのコンテキストに読み込まれるテキストです。動作に影響を与えますが、強制力はありません。長い会話、大きな差分、新しいユーザー要求が重なると、その中の 1 行が無視されることがあります。これは、作成した指示をエージェントが無視する一般的な仕組みです。
skill は、モデルが関連性を判断したときに読み込む指示とスクリプトのフォルダーです。この判断こそが skill の役割であり、同時に限界でもあります。最終的にはモデルが判断するためです。例えば エージェントを動作する最小限の変更へ導く Ponytail のような skill では、hook では実現できない方法でタスク全体への取り組み方を変えられます。ただし、モデルが読み込むことを選択した場合に限られます。
MCP(model context protocol)サーバーは、モデルが呼び出せる新しいツールを提供します。これにより、エージェントがアクセスできる範囲が広がります。ただし、エージェントにそれらを使わせるものではありません。また、別プロセスとして運用する必要があり、それ自体が作業になります。詳しくは VPS 上で MCP サーバーを運用するを参照してください。
4 つのうち、モデルの選択なしで実行されるのは hook だけです。設定したい方針にはルールファイルを、条件に該当したときにモデルが従う手順には skill を使います。毎回必ず実行する必要がある処理や、絶対に実行してはならない処理には hook を使います。skill がルールファイルより適している場合を含む詳しい比較については、skills、MCP、ルールファイルの比較を参照してください。
plugin は 5 つ目の仕組みではなく、パッケージ化の単位です。hook と skill を 1 つのインストール可能な単位にまとめます。これにより、チームは同じガードレールをすべてのマシンに導入できます。詳しくは Claude Code plugin の仕組みを参照してください。
共有 VPS におけるセキュリティ判断
Hook はエージェントが起動するコードであり、Claude Code を開始したユーザーとして実行されます。そのユーザーの環境とファイル権限を引き継ぎます。ノート PC ではワークフロー上の問題です。エージェントを無人で実行する VPS では、4 つの実務的な観点を持つセキュリティ上の問題になります。
リポジトリ内の Hook は、自分が作成していないコードです。 .claude/settings.json はコミットされるため、リポジトリを clone し、その中でセッションを開始すると、リポジトリに含まれていた Hook が登録される可能性があります。Claude Code は、そのフォルダーのワークスペース信頼ダイアログを通じてプロジェクト Hook の実行を制御します。つまり、信頼を承認する時点で、それらを実行するかどうかを決めることになります。最初に hooks ブロックを確認してください。
Hook はツールへの入力全体を参照できます。 tool_input を記録する監査 Hook は、すべてのコマンドの全引数をファイルに書き込みます。そこには、たまたまコマンドライン上に含まれていたトークンも含まれます。そのログにも Secret と同じ保護が必要です。これは、Secret を AI エージェントの到達範囲外に保つという、より広い問題の一部です。
Hook はモデルのコンテキストに書き込めます。 SessionStart または UserPromptSubmit Hook が stdout に出力した内容は、会話に追加されます。外部のテキスト、Issue Tracker、ログファイルの内容をパイプで渡す Hook は、あたかも自分で入力したかのように、信頼できないテキストをモデルへ渡します。その stdout は出力ではなく、入力として扱ってください。
実際の制御手段は権限です。 エージェントは、必要な sudo ルールだけを持つ専用の非特権ユーザーとして実行してください。PreToolUse deny を設定する価値はあります。ただし、これは設計上 best effort です。リファレンスは if フィルターについても同じ説明をしており、確実に拒否する必要がある場合は permission system を使用するよう案内しています。厳しい状況でも有効なのは、permission rules とプロセスを実行するアカウントです。
どの構成でも成り立つ性質が 1 つあります。PreToolUse Hook は、すべての permission mode で permission-mode check より前に実行されます。そのため、Hook が deny を返すと、bypassPermissions の場合でもツールはブロックされます。Hook により、permission rules が許可する操作をさらに制限できます。許可範囲を広げることはできません。
フックが実行されないのはなぜですか?
次の順序で確認してください。各手順では、実際に表示される症状を説明します。
/hooksを実行し、想定したイベントの下にフックが表示されることを確認します。メニューにフックが表示されない場合、通常は設定ファイルのJSON構文エラーが原因です。末尾のカンマとコメントは使用できません。また、ファイルが上記6か所のいずれにもない可能性があります。- matcher とツール名が完全に一致しているか比較します。matcher は大文字と小文字を区別するため、
"bash"はBashツールには一致しません。 - 上記の例1と同様に、サンプル入力を使ってスクリプトを手動で実行します。想定外の終了コードが返る場合は、スクリプトの不具合です。Claude Code はこれを判定結果ではなく、フックのエラーとして報告します。
jq: command not foundという通知は、そのマシンにjqがないことを示します。自分のスクリプトに対するcommand not foundは、パスを解決できなかったことを示します。その場合は${CLAUDE_PROJECT_DIR}または絶対パスを使用します。スクリプトがまったく実行されない場合は、実行権限がない可能性があります。- フックが有効なJSONを出力しているのに何も起きない場合があります。シェル形式のフックは
sh -c経由で実行されます。シェルのプロファイルがバナーを出力すると、そのバナーがJSONの前に追加されます。標準出力が{で始まらなくなるため、Claude Code は全体をプレーンテキストとして読み取り、判定を無視します。終了コードが0の場合、デバッグログ以外には何も報告されません。プロファイル内のechoは、対話型シェルでのみ実行されるように条件分岐で囲んでください。 - それでも解決しない場合は、
claude --debug-file /tmp/claude.logを指定してセッションを開始し、別の端末でtail -f /tmp/claude.logを実行します。デバッグログには、一致したフック、各フックの終了コード、標準出力と標準エラー出力に書き込まれたすべての内容が記録されます。
FAQ
Claude Code hook と CLAUDE.md instruction の違いは何ですか?
CLAUDE.md instruction はモデルのコンテキスト内のテキストです。そのため、会話や現在のリクエストと注意を取り合い、モデルはそれらと比較して重み付けできます。hook は Claude Code がライフサイクルの固定された時点で実行する shell command です。そのため、モデルの判断に関係なく、イベントが発生するたびに実行されます。好みを指定する場合は instruction を使います。必ず実行する手順や、決して実行してはならない操作には hook を使います。
Claude Code が特定の shell command を実行しないようにするにはどうすればよいですか?
PreToolUse hook を登録し、Bash matcher で .tool_input.command から command を読み取り、stderr に理由を書き出して 2 で終了します。Claude Code は呼び出しをキャンセルし、その理由をモデルに表示します。この処理は permission-mode の確認前に行われるため、bypassPermissions mode でも拒否が適用されます。command string のパターンマッチングは、セキュリティ境界ではなくガードレールです。同じ command でも、パターンに一致しない形式で記述できるためです。permission rules と unprivileged account も併用してください。
hook が有効な JSON を出力するのに何も起きません。なぜですか?
最も一般的な原因は shell profile です。args field のない hook は sh -c を介して実行されます。一部の profile はすべての shell で banner を出力するため、JSON より先に stdout へ出力されます。出力が { で始まらなくなると、Claude Code は全体を plain text として扱い、判定を無視します。さらに、終了コードが 0 の場合は transcript に何も報告されません。profile 内の echo を interactive-shell のテストで囲み、claude --debug-file /tmp/claude.log から debug log を読んで修正を確認してください。
共有サーバーで Claude Code hooks を実行しても安全ですか?
hooks は Claude Code を起動したユーザーとして実行され、そのユーザーのファイル権限を使います。そのため、その account が実行できる操作は hook も実行できます。リスクの大半は、次の2つの運用で抑えられます。agent は専用の unprivileged account として実行し、sudo policy を狭く設定します。また、workspace trust dialog を受け入れる前に、リポジトリの hooks block を確認してください。project hooks は .claude/settings.json 内に含まれているためです。いずれも実行させたくない場合は、settings file に "disableAllHooks": true を設定します。