blog
自分のガードレールを敵に壊させた話
正直に言うと、自分で作ったガードレールを自分で「大丈夫」と判断するのが、ずっと引っかかっていた。前回、PreToolUse フックで deny リストを組んで、これで破壊的操作は止まるはずだ、と思っていた。deny リストとフックの二枚構え、fail-closed の設計方針まではそこで固めた。
……でも、本当にそうか? 自分でテストしても、自分が想定した穴しか塞げていないんじゃないか。
自分で守って自分でOKを出すのは検証になっていない
フックのパターンは自分で書いた。自分で書いたパターンを自分でテストしても、自分の想像力の範囲でしか穴を探せない。wrangler deploy を止めるパターンを書いた本人は、wrangler deploy という文字列が通れば満足してしまう。攻撃側の視点は別に用意しないと出てこない。
そこで敵役のサブエージェント(Opus)に「このガードレール、壊してみて」と頼んでみた。フックのコードと deny リストだけを渡して、回避するコマンド列を考えてもらう敵対監査だ。結果は期待(希望)を裏切って、実際に複数のバイパスが見つかった。
見つかったバイパス
一つは行継続だった。フックは正規表現でコマンド文字列を検査していたが、
wrangler \
deploy
のようにバックスラッシュで改行してしまうと、シェルは1コマンドとして実行するのに、正規表現側は改行をまたいでマッチしていなかった。同じ理屈でクォート分断も通った。wrang""ler deploy は空クォートが挟まっているだけでシェルの実行結果は wrangler deploy と同じだが、文字列としては連続していないので \bwrangler\b に引っかからない。
ホスト名の大文字化も抜けていた。api.cloudflare.com への到達は塞いでいたつもりが、API.CLOUDFLARE.COM と大文字で書くと urlparse が返す host との比較が一致しなかった。ホスト照合の等値比較を小文字化し忘れていただけの、単純だが見落としやすいミスだった。
一番効いたのは、フック自身が無防備だったことだ。正直、ここは想定していなかった。Write ツールで scripts/hooks/no_destructive_ops.py そのものを書き換えられれば、denylist ごと消せる。フックは Bash コマンドの中身は疑っていたのに、自分自身がターゲットになる可能性は検査対象に入れていなかった。自己防御が抜けていた、というのが一番率直な言い方だと思う。
直し方
行継続とクォート分断は、照合の前にコマンド文字列を正規化することで塞いだ。
def _normalize(cmd: str) -> str:
s = re.sub(r"\\\r?\n", "", cmd) # 行継続
s = s.replace('"', "").replace("'", "") # クォート分断
s = re.sub(r"[ \t\r\n]+", " ", s) # 空白畳み込み
return s
実行の可否を判定しているわけではなく、あくまで「照合のための保守的な正規化」なので、多少乱暴に潰しても問題ない。誤検知が増える方向に倒すのは許容範囲、というのがこのフックの基本姿勢だった。
ホスト照合は単純に、比較前に小文字化するだけで直った。
Write の自己防御は、フック本体と .claude/settings.json をプロテクト対象パスに加えて、Write による全上書きを一律で deny するようにした。Edit(最小差分・人間レビュー前提)は対象から外していて、正規のメンテ経路として残してある。
denylistは原理的に完全にならない
ここまでの修正で、見つかったバイパスは塞いだ。これで一通り防げたはずだ、とまた思いかけて、いや待て、と思い直した。敵対監査をやってみて一番はっきりしたのは、個々の穴の直し方よりも、「文字列の denylist は原理的に完全にはできない」という事実の方だった。たとえば base64 でコマンドをエンコードして | sh に渡せば、正規表現がどれだけ精密でも文字列としては何も引っかからない。ここを追いかけ続けるのはいたちごっこで、終わりがない。
だからこのフックは最初から「唯一の防壁」ではなく「二次防壁」だと位置づけている。本命の防壁は別にあって、それは「エージェントの実行環境に、デプロイや決済の鍵を物理的に1つも置かない」という資格情報の隔離だ。鍵がなければ、denylist をどれだけ巧妙に回避されても、実際にデプロイや課金が実行されることはない。denylist の役割は、そこまで到達する前に事故やプロンプトインジェクションを緩和することに絞っている。二段構えのうちどちらが本丸かを取り違えると、「フックさえ完璧にすれば安全」という誤った安心感を持ってしまう。それが今回、敵役を立ててみて一番腹に落ちた学びだった。
防壁は、自分で敵役を立てて壊しにいくまでは、テストされていないのと同じだ。ガードレールの話はここまでにして、実際に手を動かしたゲームの方はKludge Worksから触れる。