Claude Codeに作業を任せているとき、ブランチ削除やforce pushのような破壊的・取り消しにくいgit操作は、実行前に必ず確認してほしいと感じる場面があった。CLAUDE.mdに一言書いておくだけでも効くが、確実性という点では心もとない。Claude Codeにはツール呼び出しの前後にコマンドを差し込めるhookという仕組みがあるので、これを使ってガードレールを作った話。
ガードレールをどこに置くか
「〜する前に確認して」というルールをどこに書くべきか、という点でCLAUDE.mdとhookの違いを整理した。
- CLAUDE.md: Claudeが読んで従うソフトなルール。書くのは簡単だが、長い会話だと読み落とし・忘れられるリスクがある
- hook(
settings.json): Claude Codeのハーネス自体が強制するルール。ツール呼び出しの前後にコマンドを差し込んで、実行をブロックしたり確認を強制したりできる
「絶対に確認なしで実行させたくない」という強制力が欲しいなら、CLAUDE.mdだけでは不十分で、hookで実装するのが確実という判断になった。今回はCLAUDE.mdに注意書きを足しつつ、実効性のあるガードとしてPreToolUseフックも用意することにした。
PreToolUseフックの実装
このリポジトリの.claude/settings.jsonにはすでにBash(git branch*)をallowするルールがあり、これがあるとgit branch -dのような破壊的なサブコマンドも無確認で実行できてしまう。そこで、Bashツールの実行前(PreToolUse)に発火するフックを追加し、破壊的なgitサブコマンドの時だけこのallowルールを上書きして確認を強制するようにした。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/confirm-destructive-git.sh",
"if": "Bash(git *)"
}
]
}
]
}
}
matcher: "Bash": Bashツールの呼び出し全般が対象if: "Bash(git *)": その中でもgitで始まるコマンドだけに絞り込む。git以外のBash呼び出しはスクリプトを起動すらしないcommand: 実際に判定を行うシェルスクリプト
判定スクリプト側では、コマンド文字列を正規表現でチェックし、branch -d/-D・push -f/--force・reset --hard・clean -f*のいずれかにマッチしたらpermissionDecision: "ask"を返す。
#!/bin/bash
set -euo pipefail
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
[ -z "$cmd" ] && exit 0
if echo "$cmd" | grep -qE '(^|[;&|]|&&)[[:space:]]*git[[:space:]].*(branch[[:space:]]+(-[a-zA-Z]*[dD][a-zA-Z]*|--delete)|push[[:space:]]+[^;&|]*(-f([[:space:]]|$)|--force([[:space:]]|$|-with-lease)|--force-with-lease)|reset[[:space:]]+[^;&|]*--hard|clean[[:space:]]+[^;&|]*-[a-zA-Z]*f)'; then
reason=$(echo "$cmd" | cut -c1-200)
jq -n --arg reason "破壊的・取り消しにくいgit操作です。実行前に内容を確認してください: $reason" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "ask",
permissionDecisionReason: $reason
}
}'
fi
PreToolUseフックは標準入力でツール呼び出しの情報(tool_input.commandなど)をJSONで受け取り、標準出力にhookSpecificOutput.permissionDecisionを含むJSONを返すことで、許可/確認/拒否を制御できる。何も出力しなければ通常の権限判定(今回で言えばallowルール)に従う。
マッチしないコマンド(git status、git branch一覧表示、git switch -cなど)は何も出力せずそのまま抜けるので、既存の運用は変えずに破壊的な操作だけをピンポイントで捕まえられる。
動作確認
フック本体はツール呼び出しJSONを標準入力に流すことでオフラインで検証できる。
echo '{"tool_input":{"command":"git branch -d feature/foo"}}' | ./confirm-destructive-git.sh
# → permissionDecision: "ask" が返る
echo '{"tool_input":{"command":"git status"}}' | ./confirm-destructive-git.sh
# → 何も出力しない
branch -d/-D・push --force・push -f・reset --hard・clean -fdはすべてask、status・branch(一覧)・switch -c・git以外のコマンドはすべて素通りすることを確認した。
まとめ
- Claudeに「〜する前に確認して」と守らせたいだけならCLAUDE.mdへの記述で足りるが、確実性が欲しいならCLAUDE.mdだけに頼らずhookで実装するのが確実
PreToolUseフックは既存のpermissions.allowルールより後段で効くので、普段はallowしたいが特定の危険なパターンだけ確認を挟みたい、という部分的なガードに向いているifでマッチャーを絞り込んでおけば、無関係なツール呼び出しのたびに判定スクリプトが起動するのを避けられる