Claude Code は本当によく働いてくれます。ファイルを直し、コマンドを叩き、テストまで回してくれる。頼もしい相棒です。
ただ、一緒に働いているとこう思う瞬間があります。
「その操作、実行する前に一回見せてほしかった」
人間なら一瞬手が止まる操作——rm -rf、git push --force、大事な記録ファイルへの直接編集——を、AIは良かれと思って、ためらいなく実行します。「このファイルは触らないでね」とお願いしても、会話が長くなると忘れます。悪気はないんです。ただ、口約束はAI相手には長持ちしません。
そこで使えるのが Claude Code の PreToolUse フックです。フックというのは、決まったタイミングに自作の処理を差し込める仕組みのこと。名前は Pre(〜の前)+ Tool Use(ツールの使用) で、Claude Code の AI はすべての操作を「ツール」経由で実行するため、「ツールを使う前」=操作が実行される直前を指します。ここに自作スクリプトを割り込ませて、「通す・止める・人間に確認を回す」を機械的に判定できるわけです。
この記事では、私が実際に運用しているフックを元に、
- 危ないコマンドを実行前に止める
- 「直接編集禁止」のファイルを守る
- 状況によってガードのON/OFFを切り替える
という3つの実例と、実運用して分かった設計のコツをまとめます。明日、あなたの Claude Code に1本目のガードを仕込めるのがゴールです。
PreToolUseフックの仕組み(30秒で)
Claude Code は、ファイル編集もコマンド実行も、すべて「ツール」と呼ばれる部品を通して行います(Bash・Write・Edit・Read など)。
PreToolUse フックは、そのツールが実行される直前に発火します。
Claude「このコマンドを実行しよう」
↓
PreToolUse フック起動(Claude Codeが何をしようとしているか伝わる)
↓
自作スクリプトが判定
├─ OK → そのまま実行される
└─ NG → 実行されず、拒否理由が Claude に返る ← ★ここがポイント
ポイントは★の行です。拒否理由は Claude 本人に届きます。 Claude はそれを読んで、別のやり方を考え直します。つまり単なる通行止めではなく、「この先は工事中です。迂回路はこちら」という看板として機能します。
最小構成で動かす
設定は settings.json(プロジェクトなら .claude/settings.json、全プロジェクト共通なら ~/.claude/settings.json)に書きます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/bash_guard.sh",
"timeout": 10
}
]
}
]
}
}
-
matcher:どのツールで発火させるか。ツール名の完全一致で、"Edit|Write"のように複数指定もできます -
command:起動するスクリプト(あなたが書く判定係です) -
timeout:秒指定。⚠ 省略すると既定は600秒(後述します)
フックが発火すると、Claude Code は「これから実行しようとしている操作の中身」を JSON にまとめて、あなたのスクリプトの標準入力へ送り込んできます。差出人が Claude Code、受取人があなたのスクリプト、という向きです。届く JSON はこんな形です(抜粋)。
{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf ./build"
},
"cwd": "/Users/you/project",
"session_id": "abc123"
}
tool_input の中身はツールごとに変わります(Bash なら command、Write なら file_path と content、といった具合です)。
JSON を受け取ったスクリプトの仕事は、中身を検査して「実行してよいか」の判定を Claude Code へ返すことです。「実行させない」と返せば、Claude Code はそのツール実行(コマンド実行やファイル編集)を取りやめます。これがこの記事で言う「止める」です。
その判定の返し方が2通りあります。
| 方式 | やり方 | 向いている場面 |
|---|---|---|
| 手軽 | exit 2 で終了し、理由を stderr へ | とにかく止めたい |
| 丁寧 | exit 0 で終了し、判定を JSON で stdout へ | 理由や代替手段まで伝えたい |
順に実例で見ていきます。
事例1:危ないコマンドを実行前に止める(exit 2方式)
まずは一番シンプルな形から。rm -rf と git push --force を含むコマンドを拒否します。
.claude/hooks/bash_guard.sh:
#!/bin/bash
payload=$(cat)
cmd=$(echo "$payload" | jq -r '.tool_input.command // ""')
case "$cmd" in
*"rm -rf"*|*"git push --force"*|*"git push -f"*)
echo "ブロックしました: $cmd" >&2
echo "破壊的な操作は禁止です。対象を限定した安全な方法を提案してください。" >&2
exit 2
;;
esac
exit 0
exit 2 で終了すると、そのツール実行はブロックされ、stderr に書いた文がそのまま Claude へのフィードバックになります。 Claude 側から見ると「実行しようとしたら断られて、理由が返ってきた」状態です。すると「では rm -rf をやめて、対象を確認しながら1つずつ削除します」のように出直してきます。
たった十数行ですが、「事故が起きませんように」という祈りが「起きない仕組み」に変わる瞬間です。
事例2:「直接編集禁止」のファイルを守る(JSON方式)
私の環境には「AIが直接編集してはいけないファイル」があります。作業記録のファイルで、決められたスクリプトを通してだけ追記してほしい。口頭で「直接書かないでね」と頼んでいた頃は、忘れた頃にやらかしてくれました。
こういう「止めたうえで、正しい道も教えたい」場面では JSON 方式が向いています。事例1の bash_guard.sh とは別の独立したフックとして作ります。settings.json の PreToolUse の配列に、もう1エントリ追加してください(フックは何本でも並べられます)。
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "python3 $CLAUDE_PROJECT_DIR/.claude/hooks/write_guard.py",
"timeout": 10
}
]
}
判定スクリプト本体の .claude/hooks/write_guard.py:
#!/usr/bin/env python3
import json
import sys
PROTECTED = ("logs/decision_log.md",)
data = json.load(sys.stdin)
path = data.get("tool_input", {}).get("file_path", "")
if path.endswith(PROTECTED):
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "decision_log.md への直接編集は禁止です",
"additionalContext": (
"このファイルへの追記は scripts/add_log.py を使ってください。"
"例: python3 scripts/add_log.py '追記したい内容'"
),
}
}, ensure_ascii=False))
sys.exit(0)
permissionDecision に指定できる主な値は3つです。
| 値 | 意味 |
|---|---|
"deny" |
実行させない。理由が Claude に返る |
"allow" |
許可の確認プロンプトを省略して通す |
"ask" |
通常の確認プロンプトを出して、人間に判断を回す |
ここで実運用上いちばん効くのが additionalContext です。この欄に書いた文は Claude の文脈に確実に入ります。 「止める」だけだと Claude は途方に暮れるか、別の抜け道を探し始めますが、「代わりにこのスクリプトを使って」と書いておくと、素直にそちらへ回ってくれます。通行止めの看板には、迂回路の地図を添える。 これだけで、拒否されたあとのAIの動きが見違えます。
事例3:状況によってガードのON/OFFを切り替える
3つ目は少し応用です。私は外出先から iPhone で Claude Code を操作することがあるのですが、小さい画面では誤操作が怖い。外出先モードのときだけ、特定の操作を止めたり確認に回したりしたいわけです。
フック自体は常設のまま、フラグファイルの有無で挙動を切り替えます。これも事例1・2とは別の独立したフックです。settings.json には、守りたいツールを matcher に並べたエントリ(例:"Bash|Write|Edit")をもう1つ足して、次の .claude/hooks/remote_filter.sh を登録します。
#!/bin/sh
payload=$(cat)
# フラグが無ければ通常モード。何もせず即通す
[ -e "$HOME/.claude/remote_mode_on" ] || exit 0
# 外出先モード中だけ、重い判定へ回す
printf '%s' "$payload" | python3 "$HOME/.claude/hooks/remote_guard.py"
モードの切り替えはコマンド1つです。settings.json を書き換える必要はありません。
touch ~/.claude/remote_mode_on # 外出先モード ON
rm ~/.claude/remote_mode_on # OFF
「フックは常に同じ判定しかできない」と思い込みがちですが、環境の状態を見て判定を変える設計にすると、適用範囲がぐっと広がります。"deny" ではなく "ask"(人間に確認を回す)と組み合わせれば、「外出先では確認多め、家では通常運転」のような運用もできます。
実運用で効いた設計のコツ4つ
3つの実例に共通して、実際に回してみて分かったことがあります。
1. 前段はシェル組み込みだけで即抜けする
matcher で絞れるのはツール名までです。つまり "Write|Edit" のフックはすべてのファイル編集で発火します。対象外の編集のたびに Python や jq を起動していると、確実に体感が重くなります(私は一度、この重さが理由で同型の仕組みを廃止したことがあります)。
対策は、前段をシェルスクリプトにして、外部コマンドを1つも呼ばずに対象かどうかを絞ることです。
payload=$(cat)
case "$payload" in
*decision_log.md*) ;; # 対象のときだけ下へ進む
*) exit 0 ;; # 対象外は即通過(外部コマンド起動ゼロ)
esac
printf '%s' "$payload" | python3 /path/to/heavy_check.py
case はシェルの組み込み機能なのでプロセスが1本も立ちません。この形にしてから、対象外の編集は1桁ミリ秒で通過するようになりました。
なお最近のバージョンには if というフィールドがあり、"if": "Edit(*.md)" のように引数レベルの絞り込みを設定側に書くこともできます。新しめの環境ならこちらも検討してください。
2. timeout は必ず明示する
省略時の既定は 600秒です。判定スクリプトが固まると、ファイル編集そのものが10分止まります。私は一律10秒にしています。
3. 迷ったら通す(フェイルオープン)
判定スクリプト自身にバグがあって「対象かどうか分からない」状態になったとき、拒否側に倒すと何も編集できなくなります。判定できないときは通す・ただしログには残す、が安全です。ルールを守らせる仕組みが本業を止めるのが、いちばん悲しい事故です。
4. 「AIに届く文」と「人間に届く文」を書き分ける
JSON 出力には似た欄がいくつもあって、届く先が違います。
| 欄 | 届く先 |
|---|---|
additionalContext |
Claude の文脈(行動を変えさせたい文はここ) |
permissionDecisionReason |
拒否理由として画面や記録に出る |
systemMessage |
人間の画面に出る警告 |
私は最初ここを取り違えて、丁寧に理由を書いたのにAIが同じ失敗を繰り返す状態を作りました。AIの行動を変えたい文は additionalContext に入れる、と覚えておくと迷いません。
ちなみに、フックは Claude Code が裏で動かすサブエージェント(並行して働く、もう1人の小さなAI)の操作にも同じように効きます。「本体は止めたのに部下が素通り」という抜け道はありませんでした。
まとめ——「お願い」から「仕組み」へ
- PreToolUse フックを使うと、AIの操作を実行される前に検査できる
- 手軽に止めるなら exit 2 + stderr、丁寧に導くなら JSON +
additionalContext - 前段はシェル組み込みで即抜け・timeout は明示・迷ったら通す、が実運用の三種の神器
AIに「気をつけてね」と言い続ける生活から、「気をつけなくても事故らない」環境づくりへ。1本目のガードは、この記事の事例1をコピーすれば10分で動きます。ぜひお手元の Claude Code に仕込んでみてください。
参考リンク
- 公式ドキュメント(Hooks ガイド): https://code.claude.com/docs/en/hooks-guide
- 公式ドキュメント(Hooks リファレンス): https://code.claude.com/docs/en/hooks
- そもそもなぜ「口で言って守らせる」のをやめたのか、という顛末は Zenn に書きました: https://zenn.dev/triponte/articles/claude-code-hook-enforce-rules