フォルダを消されたあと、settings.jsonをどう書き直したか。denyリストでは足りなかった

フォルダを消されたあと、settings.jsonをどう書き直したか。denyリストでは足りなかった AI
この記事は約13分で読めます。

前回、Claude Codeに /docs/ フォルダを消された話を書きました。サンドボックスも、コマンドの承認も、Gitも効かず、最後に助けたのはNAS宛のTime Machineだった、という記録です。あの記事の最後で「denyルールを見直した」と一行だけ書きました。では具体的に何をどう書き直したのか。今回はその中身です。

先に、いちばん大事なところを書いておきます。denyリストに危険なコマンドを並べるだけでは、同じ事故は防げませんでした。書き方に落とし穴があり、しかも私は前回の記事で、安全網の構造そのものを1つ読み違えていました。設定ファイルを開いて確かめて、それに気づきました。

訂正:サンドボックスと承認は、独立していなかった

前回、私は3枚の安全網が別々にすり抜けられた、と書きました。サンドボックス、コマンドの承認、Git。この3つが独立した網だという前提で話を組み立てていました。設定ファイルを読み直して、その前提が間違っていたと分かりました。

2行目です。autoAllowBashIfSandboxed。サンドボックスが有効なら、bashコマンドを自動的に許可する、という設定です。私自身が有効にしていました。

つまり、サンドボックスを入れたことが、承認の網を薄くしていたわけです。1枚目を張ったから2枚目が緩む。この2つは並んだ別々の網ではなく、片方がもう片方に効いてくる関係でした。守りを増やしたつもりで、実際にはトレードしていた。前回の記事で「守備範囲の違うものを並べて、数だけ数えて安心していた」と書きましたが、数え方より前に、そもそも独立していると思っていたのが誤りでした。

独立した3枚だと思っていた網が、実は1枚目と2枚目が連動していた訂正図

denyに書いた rm -rf は、たぶん今回の削除を止めない

見直したdenyリストは、いまこうなっています。

秘密情報の読み取り、外部への通信、プラグインの操作、そして削除系。並べたときは、これで削除は止まると思っていました。

ところが最後の1行をよく見ると、rm -rf / で始まるコマンドを止める書き方です。この形の指定は前方一致で効きます。rm -rf /Users/... のような絶対パスは止まる。では rm -rf docs/ は。rm -rf ./docs は。どちらも rm -rf / では始まらないので、このルールをすり抜けます。今回消えたのはワークスペース内の相対パスですから、この1行を先に書いていたとしても、同じ事故は起きたことになります。

ルート直下を吹き飛ばす事故は防げる。手元のフォルダを消す事故は防げない。そして実際に起きるのは、後者のほうです。git clean:* をdenyに入れたのは正解でしたが、これも同じ話で、消し方はいくらでもあります。rmfind -deletemv で別の場所へ飛ばす。禁止リストを長くしていく方向には、やはり終わりがありませんでした。

本命はPreToolUseフック

それで、実際の守りはdenyリストの外に置きました。

PreToolUseフックは、ツールが実行されるに、自分のスクリプトを差し込む仕組みです。matcherBash を指定しているので、Claude Codeがbashコマンドを走らせようとするたびに、このシェルスクリプトが先に呼ばれます。スクリプトの中で、そのコマンドが守りたいディレクトリに触ろうとしていないかを自分で判定して、危ないと判断したら止める。

denyリストとの違いは、判定を文字列の前方一致に頼らなくていいことです。コマンドの中身を自分のロジックで読んで、相対パスだろうと絶対パスだろうと、対象が守りたい場所かどうかで決められる。rmfind かという入口の違いにも縛られません。

スクリプトがやっていることは3段階です。Claude Codeは実行しようとしているコマンドをJSONで標準入力に渡してくるので、そこから .tool_input.command を取り出す。危ないと判定したら、permissionDecisiondeny を入れたJSONを返す。返さなければ、そのまま通ります。

判定は3つ書きました。ひとつ目が、/~$HOME.・裸のグロブに対する再帰削除。カレントディレクトリの . を含めているのは、今回まさにその形で作業フォルダごと消えたからです。ふたつ目が、プラグインの開発フォルダ名がパスの一部として出てくる破壊的な操作。みっつ目が、wp plugin install/deletegit clean です。

ふたつ目が、denyリストでは書けなかった部分です。判定の対象を「コマンドの先頭」ではなく「守りたいフォルダ名がコマンドのどこかに出てくるか」に置いています。だから rm -r でも rmdir でも mv でも rsync --delete でも find -delete でも、同じ1本のルールで引っかかる。前回書いた「消し方はいくらでもある」への答えが、ここです。入口を1つずつ塞ぐのではなく、行き先で判定する。

自分の作業を止めない工夫

ガードを書くときに面倒なのは、守りを強くするほど自分の日常の作業が引っかかることです。このプラグインのリリース作業では、配布用のZIPを作り直すために rm -f prime-cache.zip を毎回走らせます。フォルダ名がそのままファイル名の頭に入っているので、素朴に「prime-cache という文字列を含む削除」を止めると、リリースのたびに自分でブロックされます。

そこで、フォルダ名の判定は末尾がスラッシュか区切り文字のときだけ一致するようにしました。prime-cache/ は止まり、prime-cache.zip は通る。ドット1つで意味が変わります。ガードは、書いた本人が回避したくならない強さで止めるのが大事で、うるさすぎるガードは結局オフにされます。承認疲れと同じ話です。

わざと fail-open にしてある

もうひとつ、意識して決めたことがあります。このスクリプトは、判定に失敗したら通す作りにしてあります。jq が入っていなければ通す。コマンドが取り出せなければ通す。想定外の入力が来ても通す。冒頭のコメントに fail-open と書いたのはそのためです。

セキュリティの原則からすると逆で、迷ったら止める(fail-closed)が本来の作法です。それでも開ける側に倒したのは、このガードが壊れたときにセッションごと動かなくなるほうが、実害として大きいと考えたからです。すべてのbashコマンドの前に挟まる仕組みなので、ここでバグると何もできなくなります。守りのために全部止まるくらいなら、守りが空振りするほうを選びました。

この判断は、正直、正しいかどうか自信がありません。守りたかったフォルダは一度失っていて、それでもなお開ける側に倒している。安全と使い勝手のあいだのどこに線を引くかは、たぶん人によって違います。

denyとHookの比較図

承認疲れは、allowリストで減らす

前回の事故の直接の引き金は、確認ダイアログを読まずにOKを押したことでした。承認疲れです。ここへの対処は、確認を増やす方向ではなく、減らす方向に振りました。

読むだけのコマンド、状態を見るだけのコマンド、構文チェック。この手のものを片っ端からallowに入れました。git statusls で毎回確認を求められても、判断する材料は何もありません。押すだけの動作が積み重なるから、本当に読むべき確認まで同じ手つきで押してしまう。

確認の回数を減らすことが、確認の質を上げる。安全側に倒すつもりで確認を増やすと、かえって全部が素通りになる。今回いちばん腑に落ちたのが、この逆転でした。逆に ask に残したのは、外に影響が出る2つだけです。

それでも、最後の砦はバックアップ

設定を書き直しながら、結局のところ前回と同じところに戻ってきました。防ぐ側をどれだけ厚くしても、抜け道は残ります。denyリストは前方一致でしか見ない。フックは自分の書いたロジックの範囲でしか判定できない。allowを整理して確認の質を上げても、押すのは人間です。

だから、消されても戻せる状態のほうを厚くしました。前回の記事の最後に「ワークスペースの同期バックアップは検討中」と書きましたが、そのあと入れ終わったので、ここに書いておきます。restic を launchd で6時間ごとに回す構成にしました。

対象はプラグインの作業ディレクトリそのものです。保存先はTime Machineとは別のNASで、SFTP経由でリポジトリを置いています。世代は日次7・週次4・月次12・年次3で保持し、週に一度、保存済みデータの一部を読み直して壊れていないか検証する設定にしました。Time Machineとの違いは、Mac全体ではなく作業ディレクトリだけを狙うこと、そして世代を細かく持てることです。

これで、性格の違う網が2枚になりました。Mac全体を面で守る汎用の網と、作業ディレクトリだけを狙う専用の網。前回の事故で受け止めたのは前者です。専用のほうは、事故のあとに慌てて張りました。順番としては本当は逆であってほしかった、というのが正直なところです。

設定ファイルを直しても、事故が起きない保証にはなりませんでした。起きたときに、戻せる場所が増えただけです。


関連: Claude Codeにフォルダを消された。安全網は3枚あって、3枚とも素通りした(この記事の前編)

AI
この記事を書いた人
rapls

WordPressのプラグインを作っているフリーランスエンジニアです。Web開発はもう6年以上。WordPress.orgで Rapls AI Chatbot、Thanks Mail for Stripe、Rapls PDF Image Creator、Prime Cache の4本を公開し、保守を続けながら、日本語ロケールの翻訳エディター(PTE)も務めています。このブログに書くのは、現場で自分が実際にハマって、調べて、直した話です。

raplsをフォローする
raplsをフォローする

コメント

タイトルとURLをコピーしました