2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code の「ブロック済み」を1か月信じていた。フックは何も止めていなかった

2
Last updated at Posted at 2026-10-25

自分のリポジトリの 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

  1. 入力は標準入力の JSON で来る。 実行するコマンドは tool_input.command。CLAUDE_TOOL_INPUT という変数は、このページを全文検索しても見つからなかった
  2. 止める終了コードは 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

2
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?