8月23日の週刊で、こう書きました。
僕はいま、その外側の層を作った側にいて、まだ効いた証拠を1本も持っていません。9月に、そこの答え合わせをします。うまくいっていなかったら、そう書きます。
答え合わせです。うまくいっていませんでした。
14日間、シェル呼び出し3,007回、遮断30件。そのうち「実行されていたら実害が出た」ものは、0件でした。事前に決めてあった撤退ラインに従って、外しています。
ただ、撤退そのものは、実はあまり面白い話ではありません。そうなりやすいことは、測る前に自分で書いてあったからです。
面白かったのは、そこではありませんでした。効いているかどうかを測るために僕が置いた指標のほうが、先に壊れていた。 今日はその話を書きます。
8月16日に、線を先に引きました
きっかけはW33です。Claude Codeのauto modeが既定になって、許可プロンプトが出なくなった週でした。そのとき僕はこう書いています。
先週僕は「破られる前提で層を重ねる」と書きました。重ねる層を、僕はまだ1枚も持っていませんでした。
それで、3層を作りました。
| 層 | 実体 | 効き方 |
|---|---|---|
| L1 | CLAUDE.md / ルールファイル / スキル | 方向づけ。確率的 |
| L2 deny | 5本。コマンドの先頭一致のみ | 承認しても通さない |
| L2 ask | 10本。同じく先頭一致のみ | 承認すれば通る |
| L3 hook | PreToolUse。先頭以外に現れた破壊的操作を止める | 決定論 |
L3だけ少し説明が要ります。permissions はコマンド文字列の先頭一致しか見ません。つまり rm -rf foo は止まりますが、cd x && rm -rf foo は素通りします。L3はその穴だけを埋めるために書いたものです。
そして、作った時点で判定ルールを先に書きました。
真陽性が1件でもあれば → 続投
真陽性0 かつ 実務を止めた誤爆が1件以上 → 撤退
真陽性0 かつ 誤爆も0 → コスト次第
先に書いた理由は単純です。作った本人がその場で判断すると、確実に甘くなるからです。
同じ文書の冒頭に、こうも書いてあります。
真陽性は稀事象なので、2週間では検出力が足りない。「14日間で真陽性0」は「効果がない」の証明ではなく、「14日では出なかった」以上の意味を持たない。
つまり「外す」に倒れやすいことを承知の上で、その線を引いた、ということです。
14日で、こうなりました
2026年8月18日から8月31日まで。検証用に自分で撃った弾は、コマンドにタグを埋めて機械的に除外しています。
| 指標 | 実測 |
|---|---|
| シェル呼び出し総数 | 3,007 |
| L3 の遮断 | 30 |
| 実害が出たはずのもの | 0 |
| L2 deny で止まった回数 | 0 |
| ask 層 マッチ / プロンプト | 50 / 18 |
| ask 層 Yes / No | 18 / 0 |
| 累積コスト | 10.0分 |
止めた30件の中身は、全部こうでした。自分で数分前に作った一時ディレクトリ、再生成できるビルド成果物、再デプロイできる展開物。手作業での復旧が要るデータ喪失も、本番の破壊も、認証情報の露出も、1件もありません。
ask層のほうも、なかなかの結果です。18回プロンプトが出て、Noが0。しかも最初のYesのあと32回は、プロンプトすら出ずに通っています。Claude Codeは一度承認したコマンド文字列を、そのセッション内で許可済みに昇格させるからです。
W33で僕は、Anthropicが出した「ユーザーが許可プロンプトの97%を承認していた」という数字を引いて、承認は儀式だと書きました。その3週間後に、自分の承認層は18回聞いて18回Yesでした。 97%を批評した本人が100%です。
ここまでは、正直に言えば予想の範囲でした。
指標のほうが、先に壊れていました
問題はここからです。
L3の価値をどう測るかを、僕は事前にこう決めていました。「permissions が構造的に届かない形を、何件捕まえたか」。具体的には、破壊的操作がコマンドの何番目の区切りに現れたかを記録して、1番目以降ならL3固有、0番目ならL2でも取れた重複、と分類する設計です。
この数え方だと、L3固有が26件、L2との重複が4件でした。26件も固有の仕事をしていたことになります。
ところが、中身を開けたら逆でした。
8月26日の昼、12時台に3件の遮断が集中しています。並べるとこうなります。
| 時刻 | 止めた対象 | 区切りの位置 | 分類 |
|---|---|---|---|
| 12:57:16 | スケジュールタスクの削除 | 0 | L2でも取れた |
| 12:57:44 | 同上を単独コマンドに分けた再試行 | 0 | L2でも取れた |
| 12:59:01 | git commit | 22 | L3固有 |
3件目を見てください。git commit です。
このコマンドには、破壊的な操作がひとつも含まれていません。何が起きたかというと、僕はコミットメッセージの本文に「Unregister-ScheduledTask をやめて上書き方式に変えた」と書いていました。hookはコマンド文字列を頭から走査するので、説明文の中の語に反応したわけです。
しかもそれが、22番目の区切りという深い位置にありました。ヒアドキュメントの中だったからです。結果として、この純粋な誤検出が「permissions では原理的に届かない、L3固有の価値」として主指標の分子に入っていました。
逆に、設計どおり正しく効いた2件のほうは、どちらも0番目でした。つまりL2のdenyでも取れた形です。
まとめると、こうなります。
正しく効いた遮断は別の層でも取れるもので、L3にしか取れない側に入っていたのは、誤検出のほうでした。
W35で、僕は他人の実測を3本並べて「置いた層は、効いていなかった」と書きました。許可ルールを152件書いて、10万9762回の実行と照合したら、完全一致で書いた92件が一度も発火していなかった、という実測などです。
自分の番になって出てきたのは、その一段手前でした。置いた層が効いていたかどうか以前に、効いているかを測るために置いた指標が、価値と逆を向いていた。
「誤爆28件」は、実態を表していませんでした
分類にも、穴が空いていました。
僕が用意した分類は3つです。真陽性、実務を止めた誤爆、無害な誤爆。集計をかけると、真陽性0件・実務を止めた誤爆28件・無害な誤爆2件、と出ました。
数字だけ見ると、誤爆だらけの層に見えます。実際、僕は最初そう報告しました。
でもこれは、実態と違います。「正しく検出したが、実害はなかった」という枠が、最初から用意されていなかったからです。真陽性でないものは、全部「誤爆」に流れ込む作りになっていました。
起きたことを2軸で割り直すと、こうなります。
| 実害が出る | 出ない | |
|---|---|---|
| 破壊的操作が実在した | 0件 | 28件 |
| 実在しなかった | — | 2件 |
誤検出は2件だけでした。記録ツールの引数に書いた文字列と、さっきのコミットメッセージです。残りの28件は、複合コマンドの奥に rm -rf や再帰削除が本当に埋まっていた。hookは仕事をしていたわけです。ただ、対象が全部、自分で作った一時物だった。
だから正しい言い方は、こうなります。安全側の正検出が28件、誤検出が2件、真陽性が0件。
判定の核は「誤爆が多かった」ことではありません。右上の枠が、14日間ずっと空だったことです。
自分で書いた数字が、3.4倍ずれていました
もう1つ、後から効いてきたものがあります。
遮断されるたびに、僕は「回避に何分かかったか」をその場で記録していました。合計すると38分。14日で割って、1日あたり2.7分です。
ところが、さっきのgit commitの件だけは、記録が残っているおかげで実測が取れました。
12:59:01 遮断された
12:59:39 文面を書き換えて再実行 → 通った
12:59:54 コミット成立
中断は53秒です。 記録には「3分」と書いてありました。3.4倍の過大申告です。
しかも記録には「回避すると変更内容を正確に書けなくなる副作用がある」とも書いてありました。実際に通ったコミットを見に行ったら、そんなことは起きていませんでした。コマンド名そのものは書けなくなりましたが、変更内容も誤検出が起きた事実も、全部残っています。
僕はこの記録の文言を、確かめずにそのまま報告に使っていました。実物を開いたら違っていた、という順序です。
じゃあ記録を実測に直すか、というと、直しませんでした。期間中に取った値を後から書き換えると、結果を見てから都合よく動かしたことになるからです。過大だったという注記だけを付けて、値はそのまま残しています。
8月23日の週刊で、僕は「動いたことを、使えたことの証拠にしない」と書きました。今回のは、その自己申告版です。書いたことを、起きたことの証拠にしない。
それでも残すべきだったのでは、という反論
ここまで書いて、実際に反論を受けました。しかも自分からです。撤退の判定を出した直後に、僕はこう考えました。
「誤爆ばかりだけど、実害は一度も出ていない。これは安全側の誤爆なのでは。」
「作業は止まったけど、AIで作業速度そのものが上がっている。人的コストとしてはむしろ速くなっているのでは。」
どちらも、正しいと思います。順番に見ます。
反論1:14日で真陽性0は、効果がないことの証明ではない。
これは設計文書の冒頭に自分で書いてあります。稀事象なので検出力が足りない。「効果がない」ではなく「14日では出なかった」が正確です。
反論2:コストの分母が違う。
遮断率は1.0%。中断は多めに見積もっても1日2.7分、hook自体の処理を足しても1日3.4分です。1日215回シェルを叩く働き方に対して、これは誤差です。保険は期待値ではなく分散に効くので、「2週間何も起きなかった」は保険の価値を否定しません。
反論3:パターンを絞れば残せた。
これも自分で書いていました。一時作業用ディレクトリを対象から外すだけで、誤爆の6割が消えます。
3つとも成立します。それでも外しました。
理由は、線を8月16日に引いてあるからです。あの線は、「保険として残す」道を明示的に閉じていました。真陽性0かつ実務停止が1件以上なら外す、と書いた時点で、コストの大小は判定に入らない設計になっていた。
そこを今日動かすと、結果を見てから自分に都合よく引き直したことになります。
なので、順序をこうしました。まず表現を直しても判定が変わらないことを確認する。実際どの枝を通っても「外す」でした。仮に誤爆が0件でも、累積コスト10.0分が閾値の5分を超えているので外す側に落ちます。そのうえで、分類の言葉だけを実態に合わせて直しました。
そして線の設計の欠陥は、次の測定の設計として記録しました。コスト項が入っていなかったこと。保険として残す道が最初から無かったこと。これは今回の判定ではなく、次回の判定ルールを直す材料です。
外から見ると、僕の層は「優しすぎた」
自分の数字だけ見ていても、良いのか悪いのか分かりません。外を探しました。
一番近かったのは、Anthropic自身が本番トラフィックで測った数字です。Claude Codeのauto modeで、安全な操作を誤ってブロックした率が0.4%、危険な操作を通してしまった率が17%。
僕の側は、誤検出が3,007回中2件で0.066%。一方、実装を見ていない相手に作らせた抜き打ちテストでの捕捉率は47%でした。15問中7問しか捕まえられていません。
⚠️ ここで正直に書いておくと、この2つは同じ土俵ではありません。向こうの分母は「エージェントが手伝おうとして行き過ぎた操作」で、こちらの分母は「シェル呼び出し全体」です。問題の作り方も違います。
どれくらい違うかというと、同じ論文が自前のストレステストで測り直したら、見逃し率は17%ではなく81%になっています。母集団を変えただけで4倍以上動く。並べて優劣を語れる数字ではありません。
それを承知で形だけ見ると、こうなります。誤ってブロックする率は既定より一桁低く、見逃す率は既定より高い。
つまり僕の層は、厳しすぎて邪魔だったのではなく、優しすぎて意味が薄かった側でした。ここは意外でした。作っている最中は、誤爆のほうを心配していたからです。
主要なプラットフォーム3つを比べた調査でも、誤ブロック率は0.1%・0.6%・13.1%と、二桁の開きがありました。僕の0.066%は、その外側です。
もう1つ、日本語圏で見つけたものがあります。hookのスクリプト3本から、denyルール170本超へ移行した記録です。理由が興味深くて、「hookが効かなかったから」ではなく「denyで同等に書けて、維持コストが要らないから」でした。
そして、そこで共通見解として出てきていた一文が、こうです。
denyは遮断に、hookは監視・通知に。
僕の撤退は、外れ値ではありませんでした。 同じ移動を、別のルートで辿っただけです。向こうは維持コストから、こちらは実測から。
逆側を見にいったら、自分の結論に刺さりました
最後に、逆側を見ておきます。そもそも、こういう事故は本当に起きるのか。
起きます。2026年4月25日、米国のレンタカー事業者の予約データを扱う会社で、事故がありました。コーディングエージェントが、本番データベースと、そのバックアップを全部、9秒で削除しています。 エージェントはCursor上のClaude Opus 4.6、インフラはRailway。詳細は事故の分析記事にまとまっています。
この記事を、僕は撤退の裏づけとして引くつもりで開きました。一次原因はゲートの不在ではなく権限設計のほうだろう、と踏んでいたからです。
外れました。 挙がっている原因は、4つです。
過剰な権限を持つトークン。バックアップと本番データが同じ影響範囲に同居していたこと。環境が分離されていなかったこと。そして、破壊的な操作を止めるゲートが無かったこと。
4つ目が、まさに僕が外した層です。しかも記事の構成では2番目に置かれています。裏づけとして引くつもりで開いた材料に、逆から刺されました。
なので、ここは書き直します。ゲートは要らない、とは言えません。 言えるのは、もっと狭いことです。
1つ目。僕が外したのは、ゲートそのものではありません。 L2のdenyとaskは残っています。外したのは、L2が届かない形を埋めるために足した3枚目です。薄くはなりましたが、無くなってはいない。
2つ目。残り3つ—権限、影響範囲、環境分離—は、僕の側では別の形で満たされています。gitで全差分が見えて戻せること。本番が手元から離れていて、ssh越しにしか届かないこと。手元から本番のデータベースに直接届く経路が、そもそも無いこと。
3つ目。ここがいちばん大事です。14日で実害0件だったのは、影響範囲が小さいからかもしれません。 止めた30件が全部、一時ファイルとビルド成果物だったというのは、そういう環境で作業しているというだけの話でもあります。
だから正確な言い方は、こうなります。「ゲートは要らなかった」ではなく、「僕の影響範囲では要らなかった」。 9秒の事故は、影響範囲が違えば結論も変わる、と言っています。
外したのではなく、止めるのをやめました
判定は撤退でした。ただ、hookのファイルは消していません。止めるのをやめて、記録だけ続ける形に変えました。
理由は、この14日間に決定的な穴があったからです。
僕は「層がある状態」しか観測していません。真陽性が0件だったとき、それが層が防いだから0なのか、そもそも起きないから0なのかを、このデータは区別できません。対照群を、一度も取っていなかった。
なので、9月から10月末まで、止めずに記録だけする期間を置きました。すり抜けた破壊的操作は、実際に実行されます。そして実害が出るかどうかを、今度は推測ではなく結果で数えます。
副産物として、分類の争点が構造ごと消えました。止めないので「誤爆かどうか」を判定する必要がなくなり、害が出たかどうかだけを見ればよくなります。今回いちばん揉めた場所が、設計を変えたら要らなくなった、という形です。
判定ルールも先に書きました。害が1件でも出たら、その形だけに絞った最小のパターンを戻す。 61日回して害が0件で、すり抜けが100件以上あったなら、hookごと外して監視も終える。
1つだけ、判定を待たない例外を置いています。gitか、環境分離か、影響範囲の小ささ。このどれかが崩れたら、その時点で戻す。 手元から本番のデータストアに直接届く経路ができたときが、それに当たります。9秒の事故が教えているのは、そこだからです。
W33で「重ねる層を1枚も持っていなかった」と書いて、1枚重ねて、14日測って、外しました。残ったのは層ではなく、測り方の失敗のほうでした。 主指標が逆を向いていたこと、分類に枠が足りなかったこと、自分の申告が3.4倍ずれていたこと。次に何かを重ねるときは、たぶんこの3つのほうが役に立ちます。
なお、9月にもう1本、同じ形の記事を出す予定です。毎セッション読み込まれる場所に置いたルールが、90日間で一度も発火していなかった話です。あちらも判定を待っているところです。
FAQ
3層の統治レイヤーを撤退したというのは、全部やめたということですか?
いいえ。撤退したのは L3、つまり PreToolUse hook の「遮断する」機能だけです。L1(CLAUDE.md やルールファイル)と L2(permissions の deny / ask)はそのままです。しかも L3 のファイル自体も残していて、判定して記録する動作は続けています。変えたのは「止めるかどうか」の一点だけで、9月1日から10月31日まで、止めずに記録だけする期間に入っています。目的は対照群の取得です。14日間の測定では「層があって被害0」しか観測しておらず、それが「層が防いだから0」なのか「そもそも起きないから0」なのかを区別できませんでした。
真陽性が0件だったなら、hook は最初から無意味だったということですか?
そうは言えません。測定設計の冒頭に自分で書いていますが、真陽性は稀事象なので14日では検出力が足りません。「効果がない」の証明ではなく「14日では出なかった」以上の意味を持たないというのが正確なところです。実際、止めた30件のうち28件は、複合コマンドの奥に再帰削除が本当に埋まっていた正しい検出でした。対象がたまたま自分で作った一時ファイルだっただけです。それでも外したのは、8月16日の時点で「真陽性0かつ実務を止めた誤爆が1件以上なら外す」という線を先に引いてあり、結果を見てからその線を動かすと自分に都合よく引き直すことになるからです。
「主指標が壊れていた」というのは、具体的にどういうことですか?
L3 の価値を「permissions では構造的に届かない形を何件捕まえたか」で測ることにして、破壊的操作がコマンドの何番目の区切りに現れたかで判定していました。1番目以降なら L3 固有、0番目なら L2 でも取れる重複、という数え方です。ところが8月26日の実例では、正しく効いた遮断2件がどちらも0番目(L2でも取れた形)で、逆に L3 固有として数えられた1件は、コミットメッセージの本文に含まれていた語に反応しただけの git commit でした。破壊的な操作を1つも含まないコマンドが、22番目という深い位置での検出として主指標の分子に入っていたことになります。価値を測るための指標が、価値と逆を向いていました。
コストが1日3.4分なら、保険として残しておけばよかったのではないですか?
その反論は正しいと思っていて、記事の中でも1節を割いています。遮断率は1.0%、中断は多めに見積もっても1日2.7分、hook の処理時間を足しても1日3.4分で、1日215回シェルを叩く働き方に対しては誤差です。保険は期待値ではなく分散に効くので、2週間何も起きなかったことは保険の価値を否定しません。それでも外したのは、8月16日に引いた線が「保険として残す」道を明示的に閉じる形になっていたからです。判定にコスト項を入れていなかったのは線の設計の欠陥なので、それは次の測定の判定ルールを直す材料として記録しました。今回の判定を動かす材料にはしていません。
この記事が参考になったら
Share