0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

安全フックに「早く抜ける道」を足したら、守りが緩んだ

0
Posted at

Claude Code には hook という仕組みがある。設定ファイルに登録しておくと、
Claude が道具を使う直前や直後に自分のシェルスクリプトが走る。
PreToolUse に登録したスクリプトが終了コード 2 を返すと、その操作は実行前に止まる。
危険なコマンドを自分の手で止める安全網として使える。

この記事は、その安全網を自分で書いている人向けの話だ。
私は Claude Code を自動承認モード(確認を挟まない設定)で長く動かしていて、
そのために安全網を910本ほど書いて配っている。

先日、そのうちの1本に「処理を早く終わらせる関門」を足した。筋は通っていたし、
手元の試験も通った。だが CI が2件で捕まえた。関門が、そのフックが本来止めるものを
通していた。この記事はその2件の中身と、同じことを自分のフックで確かめる手順を書く。

そもそも何を直そうとしていたのか

配っている安全フックの1本が、こういうコマンドを止めていた。

echo "rm -rf / は絶対に打つな"
git commit -m "guard against rm -rf /"
grep -r "rm -rf /" .

どれも何も削除しない。危険なコマンドの名前を文字列として書いているだけだ。
検索することは実行することではないし、注意書きをファイルに書くのも実行ではない。

だが多くのフックは、渡ってきたコマンドの文字列を正規表現で見る。
引用符の中だろうが外だろうが、同じ文字列として当たる。だから止まる。

この誤検知は軽くない。止められた人はその日にフックを外す。
守りが1つ減るのは、誤検知1件よりずっと高くつく。私も自分のフックにこれで
1日7回止められたことがあって、その時の記録が残っている。

直し方は分かっていた

引用の中を空白へ潰したテキストを作り、そこに破壊的な動詞が残っているかだけを見る。

OUTSIDE=$(printf '%s' "$COMMAND" | awk -v SQ="'" -v DQ='"' '
  { line = $0; out = ""; q = 0
    for (i = 1; i <= length(line); i++) {
      c = substr(line, i, 1)
      if (q == 0 && c == SQ) { q = 1; out = out " "; continue }
      if (q == 1) { if (c == SQ) q = 0; out = out " "; continue }
      if (q == 0 && c == DQ) { q = 2; out = out " "; continue }
      if (q == 2) { if (c == DQ) q = 0; out = out " "; continue }
      out = out c
    }
    print out }')

echo "rm -rf /" はこれを通すと echo になる。rm が消える。
一方 rm -rf "/"rm -rf になり、rm は残る。

だから「潰した後に破壊的な動詞がコマンドの先頭に立っているか」を見れば、
言及と実行を分けられる。立っていなければ、後ろの判定を1つも走らせずに終わってよい。

if ! printf '%s' "$OUTSIDE" | grep -qE '(^|[;&|])[[:space:]]*(rm|rmdir|dd|chmod)([[:space:]]|$)'; then
    exit 0
fi

1本目のフックでは、これがそのまま効いた。誤検知が消えて、引用符つきの本物
rm -rf "/" のような形)は止まったままになった。試験を24件書いて、
直す前の版に当てると13件落ちることも確かめた。

2本目で、同じものが逆に働いた

同じ関門を別のフックへ入れた。手元の試験は緑だった。CIが落ちた。

FAIL: cd && git filter-repo --path foo   (expected exit 2, got 0)
FAIL: cat foo > /dev/sda                 (expected exit 2, got 0)

どちらも、そのフックが本来止めるものだ。 関門が先に exit 0 を返したので、
判定の本体に一度も届いていなかった。

原因は関門の作りにある。私は関門を「破壊的な動詞の一覧」で書いた。
ところがそのフックの判定は、動詞では表せない範囲を見ていた。

  • git filter-repo — 一覧には filter-branch を入れていたが filter-repo が無かった
  • cat foo > /dev/sda — リダイレクトでデバイスへ書く形。これは動詞ではない

つまり関門は、後ろの判定が見ているものの一部しか名指しできていなかった。

ここが一般化できる一点

早く抜ける道は、後ろにある判定の全部を名指しできる時だけ置ける。
名指しできない関門は、守りを狭めるのではなく緩める。

同じ関門が、1本目では正しく狭め、2本目では緩めた。関門の中身は同じだ。
違ったのは、後ろにある判定の広さだった。

これは速度のための最適化に限らない。「この場合は明らかに安全だから先に返す」という
枝を安全な処理に足す時、いつでも同じ形で起きる。
早期リターンは、その先を読まずに書ける。だから危ない。

私は2本目からこの関門を外した。修理して残す道もあったが、
一覧に足し続ける形は「次に何が漏れているか」を誰も答えられない。
外した理由をファイルの中に書いて、同じ試みを次に読む人が繰り返さないようにした。

自分のフックで確かめる手順

判定に渡すだけなので、危険なコマンドは一度も実行されない。

1. 後ろの判定が何を見ているか、行で数える

grep -cE 'exit 2' your-hook.sh
grep -nE 'grep -qE|case .* in' your-hook.sh | wc -l

判定の数と、関門の一覧に入れた語の数を並べてみる。
判定のほうが多いなら、その差が素通りの候補だ。

2. 関門を入れる前と後で、同じ入力を流して比べる

for f in before after; do
  git checkout "$f" -- your-hook.sh
  while read -r cmd; do
    printf '{"tool_name":"Bash","tool_input":{"command":"%s"}}' "$cmd" \
      | bash your-hook.sh >/dev/null 2>&1
    echo "$? $cmd"
  done < cases.txt > "result-$f.txt"
done
diff result-before.txt result-after.txt

差分に「2 → 0」が出たら、それが緩めた場所。 「0 → 2」は締めた場所だ。
片方だけを見ないこと。私は締めたほうだけを数えて満足していた。

3. 試験は「直す前の版に当てて落ちること」まで確かめる

通ることより、直す前に落ちることのほうが、その試験が何かを測っている証拠になる。
落ちない試験は、欠陥をそのまま仕様として記録してしまう。

4. 試験の母数を、自分の目で数える

私はここでも間違えた。「全部走らせた」と書いた53本は、試験の総数の5分の1だった。
実行スクリプトの for f in tests/*.test.sh が、tests/test-*.sh という別の命名を
拾っていなかった。実際は267本ある。

ls tests/ | wc -l          # 母数
# 自分のスクリプトが走らせた本数と、この数を突き合わせる

「全部走らせるスクリプトを書いた」は、全部走らせたことの証明ではない。

おまけ:この作業中に、同じ型の誤検知を3回踏んだ

言及と実行を見分ける関門を書いている最中に、自分の別のフックに3回止められた。

1回は正しい発火だった。残る2回は、2つのファイルを1行でステージしただけだ。

git add examples/some-hook.sh tests/some-hook.test.sh

前のファイル名の末尾の .sh と、次のファイル名の test.sh が並んで、
sh でテストを実行していると読まれた。並べる順を変えたら通った。

3回目は、この記事の下書きのファイル名だった。
...-loosens-the-guard-2026-08-12.md と付けたら、guard という語のせいで
「安全装置そのものの書き換え」と判定されて、書き込みが止まった。
名前から the-guard を外したら通った。

直している欠陥の型は、たいてい他の場所にも在る。1本直して終わりにしないほうがいい。


この手の「動いているのに間違っている」設定の事故は、症状が出ないので気づけない。
自分の環境で踏んだものを100章ぶん集めて、検出・復旧・予防の形にまとめた本がある
Anthropic公式ガイドにない事故防止・¥800)。
巻頭に「症状から該当章へ飛べる早見表」を無料で置いてあるので、
いま困っている症状があるならそこだけ見てもらえれば足りる。

フックそのものは全部無料で配っている(cc-safe-setup)。
この記事の関門も、直した後の形が入っている。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?