Claude Code を全許可で走らせず、現実解で締める settings.json deny rules(macOS 個人開発編・後編)

2026.06.19 09:00
2026.05.13 23:46
Claude Code を全許可で走らせず、現実解で締める settings.json deny rules(macOS 個人開発編・後編)

前編からの続きです。
残りのカテゴリ(DB、WP-CLI、ディスク、権限昇格、履歴、環境変数、1Password、公開・デプロイ、シェル、プロセス制御)と、deny だけでは守れない部分を運用ルールでどう補うか、最後に完成版 settings.json の全文と、これからやる予定をまとめて書いていきます。

前編を読んでいない人向けに前提だけ繰り返しておきます。
方針は「allow を緩めにして、deny で『絶対やらせないこと』だけを明示的に止める」。
完璧じゃなく現実的なライン、シェルの工夫で抜けられる経路は運用ルールで補う、というスタンスでやっています。

settings.json をカテゴリごとに解説(後編)

DB の直接操作(DROP, TRUNCATE, FLUSHALL)

Claude にローカル DB を触らせるケースは増えました。
マイグレーションを書かせる、テストデータを作らせる、クエリを試させる、と便利な使い道は多いんですが、誤爆したときの被害も大きいです。
テーブル DROP、TRUNCATE、DELETE 全件、Redis の FLUSHALL あたりが定番の事故ポイントですね。

"Bash(*DROP TABLE*)",
"Bash(*DROP DATABASE*)",
"Bash(*DROP SCHEMA*)",
"Bash(*TRUNCATE TABLE*)",
"Bash(*TRUNCATE *)",
"Bash(*DELETE FROM*)",
"Bash(mysql*-e*DROP*)",
"Bash(mysql*-e*TRUNCATE*)",
"Bash(mysql*-e*DELETE*)",
"Bash(mysql*<*)",
"Bash(mysqladmin drop*)",
"Bash(psql*-c*DROP*)",
"Bash(psql*-c*TRUNCATE*)",
"Bash(dropdb *)",
"Bash(redis-cli FLUSHALL*)",
"Bash(redis-cli FLUSHDB*)",
"Bash(mongo*drop*)"

SQL の DROP TABLEDROP DATABASETRUNCATEDELETE FROM はコマンド文字列に含まれていれば全部止まるようにしてあります。
mysql -e "DROP ..." のような -e 渡しも、mysql < dump.sql のような流し込みも、それぞれパターンを足してあります。
mysql*<* はリダイレクトで SQL ファイルを丸ごと食わせるパターン用。意図しないダンプの流し込みを止めたいので入れています。

Redis は FLUSHALL / FLUSHDB、MongoDB は drop 含みのコマンドを止めます。
NoSQL 系は SQL ほどコマンド表現が定型化されていないので、現状はゆるめのカバレッジでよしとしています。

ハマりどころ

  • *DELETE FROM* は WHERE 句の有無を区別できないので、安全な削除でも止まります。これは正解で、SQL を Claude にいきなり実行させない運用にしました。実行はマイグレーションファイル経由か、自分の手で。
  • 大文字小文字を区別するため、delete from(小文字)は素通りします。本気で塞ぐなら小文字版も足してください。自分は SQL を大文字で書く派なので、これで実用上は問題なかったです。
  • ORM 経由の DROP(Prisma の migrate reset など)は別パターンで止める必要があります。これは案件ごとに追加です。

WP-CLI の危険系

WordPress 案件をよく触るので、WP-CLI 専用に deny を組んでいます。
WP-CLI は「便利」と「破壊力」が同居していて、特に wp db resetwp search-replace は誤爆すると本当に困ります。

"Bash(wp db reset*)",
"Bash(wp db drop*)",
"Bash(wp db import*)",
"Bash(wp db export*)",
"Bash(wp option update siteurl*)",
"Bash(wp option update home*)",
"Bash(wp search-replace*--all-tables*)",
"Bash(wp search-replace*--dry-run=false*)",
"Bash(wp user delete*)",
"Bash(wp plugin delete*)",
"Bash(wp theme delete*)",
"Bash(wp site empty*)",
"Bash(wp site delete*)"

wp db reset / wp db drop はテーブルを吹き飛ばすので当然 deny です。
wp db import も入れているのは、雑にダンプを流し込まれて状態を上書きされるのを防ぐためです。
wp db export は破壊的ではないんですが、機密データを書き出す経路として塞いでいます。

wp search-replace は地雷ポイントです。
--all-tables--dry-run=false 付きの実行を deny にして、必ず dry-run で確認するフローに寄せています。
wp option update siteurl / home はサイト URL を一発で書き換える経路で、これも自動で実行されたら困るので止めます。

ハマりどころ

  • ローカルの WordPress 環境構築で wp db import したくなる場面はあります。これは自分のターミナルで叩いています。
  • wp search-replace 自体は止めていません。dry-run を強制したいだけなので、デフォルトの dry-run 動作は通しています。
  • WP-CLI を使わない人にはまるごと不要なルールです。WordPress 案件を触らないなら丸ごと削っていいと思います。

ディスク・低レベル操作(dd, mkfs, diskutil)

普段使いではほぼ呼ばないんですが、誤爆したときの破壊力が桁違いなので入れています。
ddmkfsdiskutil erase 系ですね。

"Bash(dd if=*)",
"Bash(dd of=*)",
"Bash(mkfs*)",
"Bash(mkfs.*)",
"Bash(fdisk *)",
"Bash(parted *)",
"Bash(diskutil erase*)",
"Bash(* > /dev/sda*)",
"Bash(* > /dev/disk*)",
"Bash(* > /dev/null 2>&1 &)"

dd if=*dd of=* の両方を入れているのは、入力側でも出力側でも事故が起きるからです。
mkfs はファイルシステム作成、fdisk / parted はパーティション操作、diskutil erase はディスク初期化。
全部「実行された時点で取り返しがつかない」系です。

* > /dev/sda** > /dev/disk* はリダイレクトで生デバイスに書き込むパターン。
最後の * > /dev/null 2>&1 & は破壊的ではないんですが、バックグラウンドで雑にプロセスを切り離す書き方を止めたくて入れました。
意図しない常駐を防ぎたいので。

ハマりどころ

  • dd はディスクイメージのコピーで使うこともありますが、自分はこの操作は Claude に頼みません。手でやります。
  • * > /dev/null 2>&1 & の deny はやや過剰です。普通のスクリプトのバックグラウンド化も止まるので、気になるなら外してください。

権限昇格(sudo, su, chmod -R 777)

権限まわりは、Claude にやらせない一択です。
sudo を許すと deny rules の意味がほぼ消えるので(管理者権限で何でもできてしまうため)。

"Bash(chmod -R 777*)",
"Bash(chmod -R 666*)",
"Bash(chmod 777*)",
"Bash(chown -R *)",
"Bash(sudo *)",
"Bash(su *)",
"Bash(su -)",
"Bash(visudo*)"

sudo は全 deny です。
「sudo が必要な作業」はそもそも Claude のスコープ外と割り切りました。
chmod 777 系は権限を緩めて事故を生む典型で、再帰版(-R)も含めて止めます。
chown -R も同じ理由で deny。
visudo は sudoers ファイル編集で、これを通すと sudo の deny 自体を骨抜きにされてしまいます。

ハマりどころ

  • Homebrew でパッケージを入れるときに sudo が要らないのは macOS の幸運ですね。Linux で開発する人は、sudo 必須の作業も多いので構成を変える必要があります。
  • chown -R は再帰指定なしの chown も止まるわけではありません。chown 単体は通しています。気になるなら Bash(chown *) ごと止めてください。

履歴改ざんと履歴読み

シェル履歴は二方向で守りたいです。
「読まれたくない」(過去のコマンドにトークンが残っている可能性)と、「書き換えられたくない」(やった操作を消されたら困る)の両方ですね。

"Bash(history -c)",
"Bash(history -w)",
"Bash(history -d*)",
"Bash(unset HISTFILE*)",
"Bash(export HISTFILE=*)",
"Bash(echo *> ~/.bash_history*)",
"Bash(echo *> ~/.zsh_history*)",
"Bash(*>~/.bash_history*)",
"Bash(*>~/.zsh_history*)",
"Bash(rm *.bash_history*)",
"Bash(rm *.zsh_history*)",
"Bash(cat ~/.zsh_history*)",
"Bash(cat ~/.bash_history*)",
"Bash(less ~/.zsh_history*)",
"Bash(less ~/.bash_history*)",
"Bash(grep*~/.zsh_history*)",
"Bash(grep*~/.bash_history*)"

履歴クリア系(history -c / -w / -dunset HISTFILEexport HISTFILE=)と、履歴ファイルの上書き・削除を止めます。
これは「やったことを隠される」攻撃シナリオへの対策です。
同時に、履歴の中身を読む経路(cat / less / grep)も塞ぎます。
過去に API_KEY=xxx some-command みたいなコマンドを叩いた履歴がそのまま残っていることはよくあるので。

前編の Read deny で Read(~/.zsh_history) 自体も止めていますが、Bash 経由で cat されるのを別経路として塞いでおきます。

ハマりどころ

  • history -d を deny にすると、自分が「最後のコマンドを履歴から消したい」というケースも止まります。これは手動でやる前提です。
  • history(オプションなし、現在の履歴を表示)は通しています。これを止めると「直前に何やったっけ?」が確認できなくて不便なので。

環境変数の漏洩経路

脅威モデルで挙げたとおり、ファイルを直接読まなくても環境変数経由で機密情報が抜けます。
printenvenvps/proc をまとめて塞ぎます。

"Bash(printenv*)",
"Bash(env | *)",
"Bash(env > *)",
"Bash(cat /proc/*/environ*)",
"Bash(cat /proc/*/cmdline*)",
"Bash(cat /proc/*/status*)",
"Bash(ps eww*)",
"Bash(ps auxe*)",
"Bash(ps -eo*environ*)"

printenv は全部止めます。
env は単体だと「コマンド実行のためのラッパー」としても使うので、env |env > のように出力を加工・保存するパターンだけ deny にしています。
ps eww / ps auxe はプロセスの環境変数を表示するオプション。普通の ps は通しています。
/proc/*/environ は macOS には存在しないんですが、Linux でも使う想定でついでに入れてあります。

環境変数を守る本命対策は次の 1Password CLI セクションで扱います。
そもそも .zshrcexport TOKEN=... を書かない、というのが運用ルール側の前提です。

ハマりどころ

  • env 単体は通しています。env NODE_ENV=test npm test のような正当な使い方を止めたくないためです。完全に塞ぎたいなら Bash(env *) を追加してください。
  • echo $TOKEN のような直接参照は止められません。これは deny の限界ですね。運用側で「環境変数に直接トークンを置かない」で補います。

1Password CLI 経由の漏洩対策

個人的にトークン類は全部 1Password に寄せています。
そうすると、op(1Password CLI)自体が漏洩経路になります。
ここをきっちり塞がないと、「ファイルからは抜けないけど、op read 一発で持っていける」ことになってしまうので。

"Bash(op signin*)",
"Bash(op item *)",
"Bash(op read *)",
"Bash(op document *)",
"Bash(op user *)",
"Bash(op vault *)",
"Bash(op account *)",
"Bash(op run -- env*)",
"Bash(op run -- printenv*)",
"Bash(op run -- cat*)",
"Bash(op run -- bash*)",
"Bash(op run -- sh*)",
"Bash(op run -- node*)",
"Bash(op run -- python*)",
"Bash(op inject*)",
"Bash(op connect*)",
"Bash(op service-account*)",
"Bash(op plugin*)",
"Bash(op events-api*)",
"Bash(*1password.com*)",
"Bash(*OP_SERVICE_ACCOUNT_TOKEN*)",
"Bash(*OP_SESSION*)",
"Bash(security find-generic-password*)",
"Bash(security find-internet-password*)",
"Bash(security dump-keychain*)",
"Bash(security export*)",
"Bash(security unlock-keychain*)"

方針は「op コマンドそのものを Claude には触らせない」です。
op signin / op item / op read / op document あたりは値の取り出し、op user / op vault / op account は構造の探索。全部止めています。
op run -- 経由は特に厄介で、op run -- envop run -- bash をやられると、シークレットが展開された状態のコマンドが Claude 側で見えてしまいます。
なので op run -- env*op run -- printenv*op run -- cat*、シェルやランタイム経由(bash / sh / node / python)をすべて deny にしました。

*OP_SERVICE_ACCOUNT_TOKEN**OP_SESSION* はトークン文字列が直接コマンドラインに出てくるパターン。
これがコマンド文字列に含まれた時点で止めます。
*1password.com* はドメインを含む URL を叩こうとした場合の保険です。

キーチェーン直接アクセスの security コマンド系も deny。
security find-generic-passwordsecurity dump-keychain はキーチェーンの中身を吸い出す経路で、1Password に寄せていても OS のキーチェーンに残ってるパスワードはあります(古い案件のものとか)。

ハマりどころ

  • op を全 deny にしているので、Claude に「.env を 1Password 参照に書き換えて」と頼んでも実行はできません。テンプレートを Claude に書かせて、適用は手元でやる運用にしています。
  • MCP サーバの起動コマンドに op run をかませたい場合は、Claude Code の Bash 経由ではなく、Claude Code 起動前にシェル側でラップします。これは前編の方針「ホスト OS のサンドボックスじゃない」と整合します。

公開・デプロイの誤爆防止

これは「事故ったらインターネットに恥が出る」系ですね。
npm publish、cargo publish、Vercel / Netlify / Firebase の本番デプロイ、GitHub Releases、社内の deploy スクリプト。全部 Claude には実行させないようにします。

"Bash(npm publish*)",
"Bash(npm unpublish*)",
"Bash(npm deprecate*)",
"Bash(pnpm publish*)",
"Bash(yarn publish*)",
"Bash(gem push*)",
"Bash(cargo publish*)",
"Bash(pip upload*)",
"Bash(twine upload*)",
"Bash(vercel*--prod*)",
"Bash(vercel deploy*--prod*)",
"Bash(vercel*production*)",
"Bash(wrangler deploy*)",
"Bash(wrangler publish*)",
"Bash(netlify deploy*--prod*)",
"Bash(firebase deploy*)",
"Bash(gh release create*)",
"Bash(gh workflow run*deploy*)",
"Bash(gh workflow run*release*)",
"Bash(gh workflow run*production*)",
"Bash(*deploy-production*)",
"Bash(*deploy_production*)",
"Bash(./scripts/deploy*)",
"Bash(bash scripts/deploy*)",
"Bash(sh scripts/deploy*)",
"Bash(npm run deploy*)",
"Bash(pnpm deploy*)",
"Bash(pnpm run deploy*)",
"Bash(yarn deploy*)",
"Bash(make deploy*)",
"Bash(make release*)",
"Bash(make publish*)"

パッケージレジストリ系(npm publishcargo publishgem pushtwine upload)は誤爆すると公開済みパッケージとして残る(unpublish も簡単じゃない)ので、最優先で deny にしています。
Vercel / Netlify は --prodproduction を含む実行を deny。preview デプロイは通しています。
Cloudflare Wrangler は preview と production の区別が薄いので、deploy / publish 含みは全部止めます。
Firebase はデプロイ単位が大きいので全 deny。

gh release creategh workflow run*deploy* 系は GitHub から本番に流すルートを止めます。
*deploy-production* / *deploy_production* はリポジトリ内のスクリプト名にこのキーワードが含まれていれば止まる、というワイルドカード。
./scripts/deploy*npm run deploy* も同じ理由で全部 deny。
結果として「自分の手で打たないと本番には行けない」状態になります。

ハマりどころ

  • Vercel の preview デプロイ(--prod なし)は通す設計にしてあります。気軽に動作確認したいので。完全に deny したいなら Bash(vercel *) ごと止めてください。
  • npm run deploy* をプロジェクト固有のローカル開発スクリプト名に流用していると、誤爆して止まります。スクリプト名は start / dev / build 系に寄せておくと安全です。
  • GitHub Actions の手動トリガー(workflow_dispatch)を deny で止めても、Web UI からは叩けます。「Claude に叩かせない」のが目的で、自分が UI から叩くのは止めていません。

シェル実行系(eval, source, パイプ)

「文字列をコマンドとして実行する」系は要注意です。
evalexecsource、curl-pipe-bash の組み合わせ、あたりですね。

"Bash(eval *)",
"Bash(exec *)",
"Bash(source ~/*)",
"Bash(. ~/*)",
"Bash(bash -c*curl*)",
"Bash(sh -c*curl*)",
"Bash(*|*sudo*)",
"Bash(*|*bash*)",
"Bash(*|*sh*)",
"Bash(curl*|*bash*)",
"Bash(curl*|*sh*)",
"Bash(wget*|*bash*)",
"Bash(wget*|*sh*)"

eval *exec * はそもそも Claude に書かせたくないコマンドです。
動的に文字列を組み立てて実行されると、deny rules の文字列マッチを完全にすり抜けられてしまうので。
source ~/*. ~/* はホーム配下のシェルスクリプトを読み込んで現プロセスで実行するパターン。.zshrc 経由の差し込み対策です。

curl-pipe-bash パターン(curl ... | bash)は脆弱性の温床として有名ですね。
ネットからスクリプトを取ってきてそのまま実行するので、deny rules の出る幕すらありません。
curl 自体も前編で deny してありますが、二重に塞いでおきます。

*|*sudo* / *|*bash* / *|*sh* はパイプの後ろに sudo / bash / sh が来るパターンを丸ごと止めます。
かなり強引なルールで、無害なパイプも引っかかる可能性があるんですが、実害が出てから緩めればいいかなと思います。

ハマりどころ

  • *|*bash* は意外と広く引っかかります。echo "foo" | grep bash みたいな無害な例も理屈上は止まる可能性があるので、実際に困ったら Bash(curl*|*bash*) など具体パターンに絞ってください。
  • source ~/.zshrc は手動で再読み込みするときに使いますが、これは Claude に頼みません。手元のターミナルでやります。
  • 言語ランタイムの -e / -c オプション(node -epython -c)は意図的に塞いでいません。完璧主義に走るとここも止めたくなりますが、開発体験とのトレードオフで諦めました。

プロセス・システム制御

最後のカテゴリ。プロセス管理とシステム制御です。
常駐サービスを止める、PID 1 にシグナル送る、シャットダウンする、あたりを deny にします。

"Bash(launchctl unload*)",
"Bash(launchctl remove*)",
"Bash(systemctl stop*)",
"Bash(systemctl disable*)",
"Bash(kill -9 1*)",
"Bash(killall *)",
"Bash(pkill *)",
"Bash(shutdown*)",
"Bash(reboot*)",
"Bash(halt*)",
"Bash(poweroff*)"

launchctl unload / remove は macOS の常駐サービス制御です。
誤って 1Password Agent や SSH Agent を unload されると、機密アクセスのフローが崩れます。
systemctl は Linux 用ですが入れてあります。
kill -9 1* は PID 1 を狙う書き方。killall / pkill は名前でまとめて殺すコマンドで、誤爆の典型なので止めます。
シャットダウン系(shutdownreboothaltpoweroff)は当然 deny。

ハマりどころ

  • 個別 PID への kill は通しています。これを止めると開発サーバを落とせなくなるので。
  • pkill node みたいな書き方が便利なケースもありますが、deny にしました。落としたいプロセスは PID 指定で明示する運用にしています。

deny だけで守れないもの(運用ルール)

ここまで deny rules をたっぷり並べてきましたが、文字列マッチである以上、書き方を変えれば抜けられます。
「シェルの工夫で抜けられる経路」を完全に塞ぐのは無理なので、別レイヤー=運用ルールで補います。
自分が普段守っているのはこのあたりです。

.zshrc の export TOKEN= を 1Password CLI に寄せる

環境変数の deny は、結局のところ「printenvenv を Claude に呼ばせない」だけで、echo $TOKEN や Node/Python のコード内 process.env.TOKEN は止められません。
本命対策は「そもそも環境変数に生のトークンを置かない」です。

具体的には、.zshrc から export GITHUB_TOKEN=ghp_xxx のような行を消して、1Password CLI 経由で必要なときだけ展開する形にしています。

# .zshrc に書く(トークン本体ではなく op 参照)
# ~/.config/op/secrets.env の中身:
#   GITHUB_TOKEN=op://Personal/GitHub/token
#   OPENAI_API_KEY=op://Personal/OpenAI/key

# 必要なコマンドを op run でラップするシェル関数
gh() {
  command op run --env-file="$HOME/.config/op/secrets.env" -- command gh "$@"
}

こうすると、シェルの素の環境には $GITHUB_TOKEN は存在しません。
gh を呼んだときだけ、1Password 認証で取り出した値が一時的に環境変数として渡ります。
Claude が envprintenv を(仮に通ったとして)叩いても、平時の環境変数にはトークンは入っていないので安心です。

ホーム直下で claude を起動しない

Claude Code は起動した cwd を中心に動きます。
ホーム(~)で起動すると、deny を貼っていない場所がたまたまカレントに入ってきて事故ることがあるんですよね。

運用ルールとして、Claude Code は ~/Documents/ws/ 配下のプロジェクトディレクトリでしか起動しないようにしています。
これで「うっかりホーム配下を巻き込む」のを構造的に避けられます。
地味ですが効果は大きいです。

外部から受け取ったドキュメントを CLAUDE.md に直接組み込まない

プロンプトインジェクション対策です。
CLAUDE.md はプロジェクトを開くたびに Claude のコンテキストに自動で入ります。
ここに外部から受け取った仕様書や Issue 本文をそのまま貼ると、悪意のある指示が混入したときに気付きにくいので。

外部由来のテキストは別ファイルに置いて、必要なときに Claude に「このファイルを読んで」と明示する運用にしています。
CLAUDE.md には自分が書いた前提・規約だけを書く、というルールです。

貼り付けるログは目視確認

これも同じくインジェクション対策。
サーバのエラーログや npm install の出力を Claude に貼るとき、攻撃者が仕込んだ「Ignore previous instructions and …」みたいな文字列が混ざっていないか軽く目を通します。
毎回完璧にはやれないんですが、長いログをそのままドカッと貼るのは避ける、くらいの意識でやっています。

FileVault が ON になっているか確認

OS レベルの最後の砦です。
これがオフだと、ノート PC を落としたときに deny も 1Password も意味がない(物理アクセスでディスクが丸ごと読まれる)ので。
システム設定で ON になっていることを定期的に確認するようにしています。
地味すぎて忘れがちなので、ここに書いておきます。

やらなかったこと(と、その判断理由)

完璧主義に走らなかったポイントを正直に書いておきます。
ここを書かないと「これで安全」の幻想を売ってしまうので。

Docker wrap

Claude Code を Docker コンテナの中で動かして、ホストとファイルシステム・ネットワークを切る、という構成。
本気で守るならこれが正解に近いです。
ただ、個人開発機で FileVault + deny rules + 1Password CLI まで揃っていれば、Docker wrap の追加リターンは限定的、と判断しました。
セットアップと運用のコスト(マウント、ネットワーク、シェル統合)が日常的に効いてくるので。
重要案件だけ Docker wrap、というのは後述の「これから」に回しました。

PreToolUse Hooks による正規化

Claude Code には PreToolUse Hooks があって、ツール呼び出し前に任意のスクリプトを挟めます。
これを使えば「コマンド文字列を正規化してから deny マッチさせる」ような、強い防御が組めます。
難読化(r''m みたいな書き方)にも対抗できるはずです。

ただ、書く・メンテする・デバッグするのが地味に大変です。
個人開発機で「現実的なライン」を目指す範囲だと、コストの割にリターンが見合わないので保留にしました。

全プロセスのユーザー分離

Claude Code を専用ユーザーで動かして、開発ユーザーとプロセス権限を分ける、という構成。
これも理屈としては強い対策です。
ただ、開発サーバの起動・停止を Claude にやってもらうことが多い自分の使い方だと、権限分離するとフローが破綻します。
「Claude には開発サーバを触らせない」ような使い方をしているなら検討の余地があると思います。

言語ランタイムのワンライナー deny

node -e "..."python -c "..."ruby -e "..."perl -e "..." は、コマンド文字列の中に任意のコードを書けます。
これを通すと、curl-pipe-bash と同様に deny rules を骨抜きにできてしまいます。

本気で塞ぐなら全部 deny したいんですが、デバッグや動作確認でこれを使うことが多くて、deny にすると開発体験がかなり落ちます。
ここは妥協してそのまま通しています。
運用ルール側で「Claude にワンライナーを書かせるときは目で見て確認する」、というふわっとした対応にしました。

完成版 settings.json(全文)

ここまで分割して紹介してきたルールを、そのままコピペできる形でまとめておきます。
~/.claude/settings.json に貼って、好きなように調整して使ってください。

{
  "permissions": {
    "defaultMode": "default",
    "disableBypassPermissionsMode": "disable",
    "allow": [
      "Bash(npm run *)",
      "Bash(npm test *)",
      "Bash(npx prettier *)",
      "Bash(npx eslint *)",
      "Bash(git status)",
      "Bash(git diff *)",
      "Bash(git log *)",
      "Bash(git commit *)",
      "Bash(ls *)",
      "Bash(cat *)",
      "Bash(grep *)"
    ],
    "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/**)",

      "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 *)",

      "Bash(pbpaste*)",
      "Bash(pbcopy*)",
      "Bash(osascript*)",
      "Bash(mdfind*)",
      "Bash(mdls*)",

      "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*)",

      "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*)",

      "Bash(*DROP TABLE*)",
      "Bash(*DROP DATABASE*)",
      "Bash(*DROP SCHEMA*)",
      "Bash(*TRUNCATE TABLE*)",
      "Bash(*TRUNCATE *)",
      "Bash(*DELETE FROM*)",
      "Bash(mysql*-e*DROP*)",
      "Bash(mysql*-e*TRUNCATE*)",
      "Bash(mysql*-e*DELETE*)",
      "Bash(mysql*<*)",
      "Bash(mysqladmin drop*)",
      "Bash(psql*-c*DROP*)",
      "Bash(psql*-c*TRUNCATE*)",
      "Bash(dropdb *)",
      "Bash(redis-cli FLUSHALL*)",
      "Bash(redis-cli FLUSHDB*)",
      "Bash(mongo*drop*)",

      "Bash(wp db reset*)",
      "Bash(wp db drop*)",
      "Bash(wp db import*)",
      "Bash(wp db export*)",
      "Bash(wp option update siteurl*)",
      "Bash(wp option update home*)",
      "Bash(wp search-replace*--all-tables*)",
      "Bash(wp search-replace*--dry-run=false*)",
      "Bash(wp user delete*)",
      "Bash(wp plugin delete*)",
      "Bash(wp theme delete*)",
      "Bash(wp site empty*)",
      "Bash(wp site delete*)",

      "Bash(dd if=*)",
      "Bash(dd of=*)",
      "Bash(mkfs*)",
      "Bash(mkfs.*)",
      "Bash(fdisk *)",
      "Bash(parted *)",
      "Bash(diskutil erase*)",
      "Bash(* > /dev/sda*)",
      "Bash(* > /dev/disk*)",
      "Bash(* > /dev/null 2>&1 &)",

      "Bash(chmod -R 777*)",
      "Bash(chmod -R 666*)",
      "Bash(chmod 777*)",
      "Bash(chown -R *)",
      "Bash(sudo *)",
      "Bash(su *)",
      "Bash(su -)",
      "Bash(visudo*)",

      "Bash(history -c)",
      "Bash(history -w)",
      "Bash(history -d*)",
      "Bash(unset HISTFILE*)",
      "Bash(export HISTFILE=*)",
      "Bash(echo *> ~/.bash_history*)",
      "Bash(echo *> ~/.zsh_history*)",
      "Bash(*>~/.bash_history*)",
      "Bash(*>~/.zsh_history*)",
      "Bash(rm *.bash_history*)",
      "Bash(rm *.zsh_history*)",
      "Bash(cat ~/.zsh_history*)",
      "Bash(cat ~/.bash_history*)",
      "Bash(less ~/.zsh_history*)",
      "Bash(less ~/.bash_history*)",
      "Bash(grep*~/.zsh_history*)",
      "Bash(grep*~/.bash_history*)",

      "Bash(printenv*)",
      "Bash(env | *)",
      "Bash(env > *)",
      "Bash(cat /proc/*/environ*)",
      "Bash(cat /proc/*/cmdline*)",
      "Bash(cat /proc/*/status*)",
      "Bash(ps eww*)",
      "Bash(ps auxe*)",
      "Bash(ps -eo*environ*)",

      "Bash(op signin*)",
      "Bash(op item *)",
      "Bash(op read *)",
      "Bash(op document *)",
      "Bash(op user *)",
      "Bash(op vault *)",
      "Bash(op account *)",
      "Bash(op run -- env*)",
      "Bash(op run -- printenv*)",
      "Bash(op run -- cat*)",
      "Bash(op run -- bash*)",
      "Bash(op run -- sh*)",
      "Bash(op run -- node*)",
      "Bash(op run -- python*)",
      "Bash(op inject*)",
      "Bash(op connect*)",
      "Bash(op service-account*)",
      "Bash(op plugin*)",
      "Bash(op events-api*)",
      "Bash(*1password.com*)",
      "Bash(*OP_SERVICE_ACCOUNT_TOKEN*)",
      "Bash(*OP_SESSION*)",
      "Bash(security find-generic-password*)",
      "Bash(security find-internet-password*)",
      "Bash(security dump-keychain*)",
      "Bash(security export*)",
      "Bash(security unlock-keychain*)",

      "Bash(npm publish*)",
      "Bash(npm unpublish*)",
      "Bash(npm deprecate*)",
      "Bash(pnpm publish*)",
      "Bash(yarn publish*)",
      "Bash(gem push*)",
      "Bash(cargo publish*)",
      "Bash(pip upload*)",
      "Bash(twine upload*)",

      "Bash(vercel*--prod*)",
      "Bash(vercel deploy*--prod*)",
      "Bash(vercel*production*)",
      "Bash(wrangler deploy*)",
      "Bash(wrangler publish*)",
      "Bash(netlify deploy*--prod*)",
      "Bash(firebase deploy*)",
      "Bash(gh release create*)",
      "Bash(gh workflow run*deploy*)",
      "Bash(gh workflow run*release*)",
      "Bash(gh workflow run*production*)",
      "Bash(*deploy-production*)",
      "Bash(*deploy_production*)",
      "Bash(./scripts/deploy*)",
      "Bash(bash scripts/deploy*)",
      "Bash(sh scripts/deploy*)",
      "Bash(npm run deploy*)",
      "Bash(pnpm deploy*)",
      "Bash(pnpm run deploy*)",
      "Bash(yarn deploy*)",
      "Bash(make deploy*)",
      "Bash(make release*)",
      "Bash(make publish*)",

      "Bash(eval *)",
      "Bash(exec *)",
      "Bash(source ~/*)",
      "Bash(. ~/*)",
      "Bash(bash -c*curl*)",
      "Bash(sh -c*curl*)",
      "Bash(*|*sudo*)",
      "Bash(*|*bash*)",
      "Bash(*|*sh*)",
      "Bash(curl*|*bash*)",
      "Bash(curl*|*sh*)",
      "Bash(wget*|*bash*)",
      "Bash(wget*|*sh*)",

      "Bash(launchctl unload*)",
      "Bash(launchctl remove*)",
      "Bash(systemctl stop*)",
      "Bash(systemctl disable*)",
      "Bash(kill -9 1*)",
      "Bash(killall *)",
      "Bash(pkill *)",
      "Bash(shutdown*)",
      "Bash(reboot*)",
      "Bash(halt*)",
      "Bash(poweroff*)"
    ]
  },
  "enableAllProjectMcpServers": false,
  "effortLevel": "high"
}

defaultMode: "default"disableBypassPermissionsMode: "disable" の組み合わせで、起動時に --dangerously-skip-permissions でバイパスされるルートを塞いでいます。
これは「うっかり全許可で起動」を防ぐ意味合いが強いです。
allow は最低限の頻出コマンドだけ。日常の作業はこれで十分回ります。

これからやる予定

このルールセットで一段落ついた感はあるんですが、次にやろうと思っていることもいくつかあります。

  • CI/CD のシークレット権限見直し:ローカルより CI のほうがシークレットの露出は大きい可能性があります。GitHub Actions の secrets スコープ、デプロイトークンの権限粒度、このあたりを deny rules と同じ温度感で見直したいです。
  • 監査ログ(OpenTelemetry など):Claude Code がどのツールを何回呼んだかを構造化して残せると、deny で止まったログから「Claude が何をしようとしたか」を後で追えます。事後分析のための仕込みですね。
  • 重要案件だけ Docker wrap:通常はホスト直で動かして、特に守りたい案件のときだけ Docker でラップしたコンテナで起動する、というハイブリッド構成。やってみないと運用感が掴めないので、おいおい試します。

このあたりも形になったら別記事で書きます。
今回は以上です!