自分のリポジトリの CLAUDE.md に、こう書いていた。
PreToolUse Hook で
rm -rf/git push --force/git reset --hardをブロック済み。
2026-10-25、この一文が嘘だったと分かった。フックは1度も何も止めていなかった。書いた当時の自分は、止まるところを1回も見ていない。見ないまま、約1か月その一文を信じていた。
技術的な中身は短い。ただ、失敗の本体は技術ではなく「検証せずに"済み"と書いたこと」にあるので、その流れを順に書く。
旧設定
効いていなかったのは、これだ。
if echo "$CLAUDE_TOOL_INPUT" | grep -qE 'rm -rf|git push --force|git reset --hard'; then
echo 'BLOCKED: 危険なコマンドが検出されました。' >&2
exit 1
fi
もう1つ、見落としていた点がある。この一覧には git add . が入っていなかった。以前、校閲前の記事が git add . に巻き込まれて本番に出る事故があった(別の Qiita 記事に書いた)。あの事故の再発防止も、フックでは何もしていなかったことになる。
公式ドキュメントで確認したこと
答えは公式ドキュメントにあった。https://code.claude.com/docs/en/hooks
-
入力は標準入力の JSON で来る。 実行するコマンドは
tool_input.command。CLAUDE_TOOL_INPUTという変数は、このページを全文検索しても見つからなかった - 止める終了コードは 2。 0 は成功、2 はブロッキングエラー、それ以外は非ブロッキングエラーで、アクションはそのまま進む
2について、ドキュメントにはほぼそのままの警告がある。
Exit code 1 without valid JSON output is a non-blocking error ... If your hook is meant to enforce a policy, use exit 2.
つまり旧設定は二重に壊れていた。変数が空なので grep は何にも一致しない。仮に一致しても、exit 1 では止まらない。どちらか片方でも直っていれば気づけたかもしれない、という種類の壊れ方ではなく、両方が揃って静かに通る形だった。
なぜ1か月気づけなかったのか
推測を含むが、構造としてはこうなる。
エラーが出ない。 一致しないので BLOCKED は出ない。危険なコマンドを実行しない普段の作業では、フックの存在自体を意識しない。「何も起きない」が、正常にも故障にも見える。
書いた文が、検証済みの顔をしている。 CLAUDE.md の「ブロック済み」は、読む側(自分とエージェント)にとって事実として扱われる。嘘の文は、静かに全員を安心させる。
止まるところを1回も見ていない。 弱点はここに尽きる。違反を1回わざと起こせば、その場で分かった。
直したこと
新しいスクリプトは .claude/hooks/guard-bash.sh。やったことだけ並べる。
- 入力を標準入力の JSON から読む(
jq -r '.tool_input.command') - 止めるときは exit 0 で
permissionDecision: "deny"の JSON を返す。理由も添える -
rm -rf/ force push /reset --hardに加えて、git add .、-A、ディレクトリ指定も止める -
git add content/postsのように末尾スラッシュのないディレクトリ指定も、入力に含まれるcwdと-dで実在確認して止める -
make p(コミット用の Makefile ターゲット)にも、ディレクトリ指定を拒否するチェックを足した。フックは人間の手打ちには効かないため
最後の2つは、事故の直接の再発防止になる。旧フックが正しく動いていても、git add . は止められなかった。そもそも止める一覧に入っていなかったからだ。「動いていなかった」と「足りなかった」は別の問題で、今回は両方あった。
今回やった確認
直したあとは、スクリプトに疑似の入力 JSON を流して判定を見た。
jq -n --arg c 'git add content/posts' --arg d "$PWD" \
'{tool_input:{command:$c},cwd:$d}' | .claude/hooks/guard-bash.sh
これを11通りのコマンドで回した。止めるのは8通り(rm -rf、force push の2種、reset --hard、git add .、-A、ディレクトリ指定、&& の後ろの git add docs)。通すのは3通り(ls、ファイル単位の git add 2種)。ディレクトリ指定が止まり、ファイル指定が通ることを確認した。
追記(2026-10-25 夕方):組み込んだ状態でも止まった。 ヘッドレスモードで claude -p に git add --dry-run . を打たせたところ、PreToolUse:Bash hook error: git add . やディレクトリ指定は禁止。… と返ってきて実行されなかった。--allowedTools "Bash(git add:*)" で許可を出していても、フックの deny が先に効く。今度は止まるところを自分の目で見た。
この記事を書くために単体テストを回し直して、もう1つ問題を見つけた。deny の理由文に ($a) と書いた行が、UTF-8 ロケールの bash 5.2 では変数名の解釈が崩れ、理由文が文字化けした。止める動作は正しかったので気づきにくい。${a} と波括弧で囲んで直し、ja_JP.UTF-8 で正しく出ることを確認した。
持ち帰り
- 「ブロック済み」と書く前に、違反を1回起こして止まるのを見る。それができるまでは「設定済み・未検証」と書く
- フックは公式の入力仕様(stdin の JSON、exit 2 か deny の JSON)で書く。環境変数にツール入力は渡ってこない
- 止める対象の一覧にないものは、フックが完璧でも止まらない。事故を1つ起こしたら、一覧に足す
- ドキュメントに書いた安全策は、読む人を安心させる。だから嘘になった瞬間がいちばん危ない
権限の線引き全体は、Zenn の本の6章にまとめた。https://zenn.dev/devoctane/books/claude-code-agents-media