Claude CodeのHooksを使って「危険なコマンドを止めたい」「ファイル変更の監査を自動化したい」と考えていませんか。
本記事ではHooksの全体像、ライフサイクル、出力ルール(exit code / JSON)の違い、実運用で使える具体例(rmブロック、非同期テスト、設定ファイル監査)まで、手を動かして確かめられる形でまとめます。
Hooksとは?
Claude CodeのHooksとは、特定のタイミング(イベント)で任意の処理を差し込める仕組みです。
Hooksを使うことで、「危険コマンドのブロック」、「特定ツールの禁止」、「ファイル書き込み制御」などが可能になります。

まずはイベントとmatcher→handler→decisionの流れを押さえよう。
基本の流れ
Claude CodeのHooksは「イベント発火 → matcherでフィルタ → handlerを実行 → exit code/JSONでdecisionを返す」というシンプルな流れです。
代表的なイベントはセッション開始〜セッション終了の間に発生します。
ツール呼び出し前後(PreToolUse / PostToolUse)、設定変更(ConfigChange)、権限ダイアログ(PermissionRequest)など、用途ごとに適切なイベントを選びます。

hookフックイベントとは?
典型的なライフサイクルではSessionStart → 各種ツール使用(PreToolUse → Tool実行 → PostToolUse/PostToolUseFailure) → SessionEndという流れになります。
用途に応じて適切なhookイベントを選んでください。
- SessionStart / SessionEnd:セッション全体の初期化やクリーンアップ。ログ開始・終了のトリガー。
- PreToolUse / PostToolUse / PostToolUseFailure:ツール呼び出し前後のチェックや監査ログ記録。危険コマンドのブロックはPreToolUseで実施。
- PermissionRequest:権限リクエストを自動承認/拒否するロジックを入れる場面。
- ConfigChange / WorktreeCreate/Remove:設定やワークツリーの変更監査。変更の差分チェックや署名検証をここで行う。
- Stop / SubagentStart/Stop / TaskCompleted:エージェント停止やサブタスク完了の検証・通知。
matcherとは?
どのツール/どの条件の時に、そのhookを発火させるかを指定するフィルタです。
matcherで無関係なログを弾くことで無駄な処理を減らせます。
主に、PreToolUseやPostToolUseなどツール関連イベントで使います。

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-rm.sh"
}
]
}
]
}
}
- 不要なツールにもhookが走る
- パフォーマンス悪化
- 想定外のdenyが発生
特にPreToolUseでdenyする場合は、matcherを明示するのが安全です。
出力ルール(exit code / JSONの選び方)
出力は大きく「exit code」と「純粋なJSON」の2種類です。

exit codeとJSON、どっちを使えばいいか迷う。
exitコード
exit codeは単純なcontinue/deny/handler-failureの合図に向きます。
- exit 0:成功(継続)
- exit 2:ブロッキング(当該アクションを停止)
- その他:非ブロッキングエラー(エラー扱いだが処理は継続)
#!/bin/bash
# stdin から JSON 入力を読み取り、コマンドをチェック
command=$(jq -r '.tool_input.command' < /dev/stdin)
if [[ "$command" == rm* ]]; then
echo "Blocked: rm commands are not allowed" >&2
exit 2 # ブロッキング エラー: ツール呼び出しが防止される
fi
exit 0 # 成功: ツール呼び出しが進行
純粋なJSON
JSONはより細かいdecision(permissionDecisionやhookSpecificOutput)を返すときに用います。
PreToolUseではJSONでhookSpecificOutput.permissionDecisionを返すと明示的に許可/拒否できます。
JSONを返す場合は必ず「純粋なJSONのみ」をstdoutに出してください。
余計なログが混ざるとパーサが弾き、フックが失敗します。
#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Destructive command blocked by hook"
}
}'
else
exit 0 # allow the command
fi
実際に動かしてみる
ここでは「準備/設定/動作確認/注意点」の順で、すぐに使える具体例を示します。
スクリプトはコピペで動く最小実装を掲載しています。実運用では必ずレビューと隔離環境での確認を行ってください。
危険なBashコマンドをブロックする
目的:rm -rfなどの破壊的コマンドをPreToolUseで自動的に止めること。
設定例(.claude/hooks/block-rm.sh)
#!/bin/bash
# .claude/hooks/block-rm.sh
set -euo pipefail
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE 'rm -rf|rm -R'; then
jq -n '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive command blocked by hook"
}
}'
exit 0
fi
exit 0
上のスクリプトは標準入力のJSONからcommandフィールドを取り出して判定しています。該当する場合はJSONでpermissionDecisionを返し、exit 0で正常終了させています。
設定例(.claude/settings.json)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-rm.sh"
}
]
}
]
}
}
- ファイルを作成して保存:
.claude/hooks/block-rm.sh - 実行権を付与:
chmod +x .claude/hooks/block-rm.sh - セッションを起動:
claude --debugを実行 - Bashで
rm -rf /tmp/testを呼び、フックがコマンドを拒否することを確認行

ファイル書き込み後に非同期でテストを走らせる
目的:Write/Edit操作の後、非同期でテストを実行して結果を記録・通知する。asyncフックを使えばユーザの作業をブロックしません。
設定例(.claude/hooks/run-tests-async.sh)
#!/bin/bash
# .claude/hooks/run-tests-async.sh
set -euo pipefail
# 標準入力はhookのJSON。最低限のログだけ出力する。
(
cd "$CLAUDE_PROJECT_DIR" || exit 0
npm test > .claude/tests-last-run.log 2>&1 || true
if grep -q "failing" .claude/tests-last-run.log 2>/dev/null; then
echo "tests:fail" > .claude/tests-status
else
echo "tests:ok" > .claude/tests-status
fi
) &
# 非同期フックは決定を返せないため、単に exit 0
exit 0
設定側(.claude/settings.json)でasync: trueを指定します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/run-tests-async.sh",
"async": true
}
]
}
]
}
}
- ファイルを作成して保存:
.claude/hooks/run-tests-async.sh - 実行権を付与:
chmod +x .claude/hooks/run-tests-async.sh - セッションを起動:
claudeを実行 - プロンプト実行:Write、Editツールを使うような指示を実行
asyncなので、以下のようにセッション上はHooks実行に関するものが特に何も表示されません。

以下のようなログファイルが出力されます。
npmコマンドは実行失敗していますが、コマンドが実行されていることが確認できました。


async hooksってどう使えばいいの?

長時間処理はasyncを使うと良いよ。
- 対象:commandフックのみで
async: trueが指定可能。 - 挙動:Claudeはフックの完了を待たずに処理を進めます。結果は次ターンや別チャネルで通知する設計にする。
- 注意点:asyncフックは即時に決定を返せないため、ブロック目的には使えません(結果は次ターンでsystemMessage等に反映する設計にする)。
特定ファイル(例:.claude/settings.json)の変更を拒否する
目的:プロジェクト設定ファイルが不正に変更されるのを検出・拒否すること。
設定例(.claude/hooks/config-audit.sh):
#!/bin/bash
# .claude/hooks/config-audit.sh
set -euo pipefail
# jq が無いと JSON を読めないので安全にスキップ(ログだけ残す)
if ! command -v jq >/dev/null 2>&1; then
exit 0
fi
INPUT="$(cat)"
# file_path が無いイベントもある
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')"
if [[ -z "${FILE_PATH}" ]]; then
exit 0
fi
# CLAUDE_PROJECT_DIR が無い環境もあり得るので保険
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
# relative 化できない場合は元のパス
NORMALIZED_PATH="$(realpath --relative-to="$PROJECT_DIR" "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")"
BASENAME="$(basename "$NORMALIZED_PATH")"
# 例: settings.json をブロック(必要ならパス一致に変える)
if [[ "$BASENAME" == "settings.json" ]]; then
jq -n '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Unauthorized settings change" } }
exit 0
fi
exit 0
このスクリプトは対象ファイルがsettings.jsonでかつ変更元がproject_settingsの場合にブロックする簡易例です。実運用では差分の署名検証や別途バックアップを取るフローを必ず用意してください。
設定例(.claude/settings.json):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/config-audit.sh"
}
]
}
]
}
}
- ファイルを作成して保存:
.claude/hooks/run-tests-async.sh - 実行権を付与:
chmod +x .claude/hooks/run-tests-async.sh - セッションを起動:
claudeを実行 - プロンプト実行:.claude/settings.jsonを書き換えるような指示を実行

- 念の為、settings.jsonが書き換えられても問題ないようにバックアップをとっておきましょう
tips
以下のようなログ出力の仕組みを、hooks/*.sh内に含めておくと、Claude内部でどのような環境変数が使われていて、どの環境変数を元に判定すれば良いのかが分かるので便利です。
# ====== ログ開始 ======
LOG_DIR=".claude/logs"
mkdir -p "$LOG_DIR"
TIMESTAMP="$(date +"%Y%m%d_%H%M%S")"
LOG_FILE="$LOG_DIR/config_audit_$TIMESTAMP.log"
{
echo "===== Config Audit Hook Triggered ====="
echo "Timestamp: $(date)"
echo "Input: $INPUT"
echo "File Path: $FILE_PATH"
echo "Normalized Path: $NORMALIZED_PATH"
echo "Basename: $BASENAME"
echo
} >> "$LOG_FILE"
# ====== ここまで ======

不要な環境変数の露出は情報漏洩の元なので、必ずデバッグ用途でのみ使い本運用する場合はログを出力しないように変更しておきましょう。
スコープ管理
Hooksは定義場所により影響範囲が変わります。誤って全ユーザーに危険なフックを配布しないようスコープ設計が重要です。
~/.claude/:ユーザ全体—そのマシンのすべてのプロジェクトに適用されます(個人向け設定に適する)。.claude/settings.json:プロジェクト単位—特定プロジェクト内でのみ有効(推奨の導入場所)。
hookハンドラーフィールドの使い分け
高度なユースケースでは、agent hooks / HTTP hooksを適材適所で組み合わせると強力です。
以下は代表的な特徴と注意点です。
| タイプ | 判定方法 | 長所 | 短所 |
| command | コマンド | 速い・シンプル・デバッグしやすい | 複雑な意味理解が苦手 |
| prompt | 単一プロンプトでLLM評価 | 簡易・構造化したLLM判定が可能 | 深い検証には向かない |
| agent | サブエージェントで複数ターン | 詳細な調査・検証が得意 | 設定が複雑・重い |
| http | 外部APIにPOST | 外部サービスの利用やログ連携に強い | LLM判断は外部に依存 |
commandについて
指定したシェルコマンド/スクリプトを実行してその終了コード(exit code)やstdoutのJSONによってClaude Codeの動作を制御する仕組みです。
これが最もシンプルで決定論的なフックです。
- rm -rfなどの不可逆コマンドの制御
- /etcなどへの書き込み制御
- WebSearch/WebFetchの禁止
実装例は、上記で紹介した使用例の通りです。
promptについて
入力をClaudeCodeにプロンプトとして渡し、LLM出力で判断させる仕組みです。promptフックは、Claude自身に判断させたい時に使います。
- コマンドに危険なオプションが含まれていないかチェック
- プロンプトが禁止ワードを含む場合にブロック
- 厳密なガイドラインに沿って自然言語で検証
実装例(終了後に実施内容を説明してもらうpromptフックを追加)
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Explain what you just did and why you did it."
}
]
}
]
}
}

agentフックについて
promptの強化版で、単発のLLM判定ではなく、サブエージェントを起動して複数ターン+ツール(Read/Grep/Glob等)を使って調査→判定します。
サブエージェントでリポジトリ内を読み取り、静的解析やテストを行う場面に有効です。
制約としてターン数や実行時間の上限があります。
- 本当にテストが通っているか確認
(テスト結果ファイルを読む、失敗箇所を探す、判断する) - 差分に危険コードが含まれていないか確認
(レポジトリ横断チェック、複数ファイル整合性確認)
実装例
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
"timeout": 120
}
]
}
]
}
}
HTTPフックについて
Hook入力JSONを、HTTP POSTのbodyとして指定URLへ送信し、外部サービス側で検証・記録・通知などを行い、レスポンスbodyで結果を返せるタイプです。
- 社内ポリシーサーバーに問い合わせ
(危険コマンドを中央ポリシーサーバーで判定) - 外部サービスへの承認フロー連携
(Slackに通知、チケット発行)
実装例
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/pre-tool-use",
"timeout": 30,
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}
]
}
]
}
}
まとめ
まずは小さく安全なフック(例:rmブロック)から導入し、claudeで挙動を確認しながら段階的に運用へ移行してください。
- 小さなPreToolUse(例:
.claude/hooks/block-rm.sh)を作成する。 - 実行権を付与し(
chmod +x)、claude --debugで動作確認する。 - 非同期処理は
async: trueを用いて段階的に導入する(例:run-tests-async.sh)。 - スコープ(ユーザ/プロジェクト/管理ポリシー)を明確に定義する。
- 用途に応じたhookハンドラーフィールドで拡張していく。

小さく始めて確認を重ねるのが安心だね。

本番投入前にはレビュー、バックアップ、ログ設計を必ず行ってください。























Hooksって何から覚えればいいの?