新しいプロジェクトで settings.json を開くと、permissions セクションはたいてい空っぽだ。allow も deny もまだ何も書いていない。この最初の空白に、何をどんな順番で書き足していくかで、その後の運用がだいぶ変わるんですよね。
先に断っておくと、この記事は「Hooksを全部deny rulesに置き換えた話」でも「settings.json全体を半年運用してわかった罠」でもない。そのあたりはすでに書いた(末尾にリンクを置く)。今回はもっと手前、deny・allow・askという3つの選択肢を何を基準に振り分けるか、そしてワイルドカードパターンをどう書けば安全なのかを、空のpermissionsから組み立てる手順としてまとめる。「permissionsだけを、リファレンスとして読める形にする」のが今回のゴール。
この記事で学べること
- 破壊的操作・秘匿情報アクセス・ネットワークアクセス・システム設定変更という4つのリスクカテゴリでdenyを整理する考え方
- allow・deny・askを可逆性、影響範囲、頻度で振り分ける決定木
- ワイルドカードパターンが素通りする典型パターンと、防ぐための書き方
- 空の
permissionsから段階的にルールを積み上げていく手順
前提
- Claude Code(Pro $20/月、Max $100〜$200/月)の
permissions.denyをまだ数行しか書いていない、あるいはこれから初めて書く人を想定している - hooksの実装方法やeffortLevelの調整は扱わない。あくまで
permissionsという1フィールドの設計基準の話
リスクカテゴリで考える: 何から deny すべきか
permissions.denyに思いついた順でパターンを足していくと、そのうち「これ本当に必要?」を誰も説明できない行が増えていく(レビューで指摘されるたびに「たぶん昔なにかあったんだと思います」で押し切るやつ)。先にリスクを4つのカテゴリに分けておくと、新しいパターンを足すときに「これはどの棚に入るか」で迷わなくなる。
| カテゴリ | 典型的な危険操作 | 設計の勘所 |
|---|---|---|
| 破壊的操作 | rm -rf、git push --force、保護ブランチへの直push、ディスクフォーマット系コマンド、強制kill・shutdown系 | 取り返しがつかないので最優先でdeny。副作用が同じ系統のコマンド(/bin/rm、dd、mkfs等)も併せて潰す |
| 秘匿情報アクセス | .env・.ssh・.aws・.gnupgの読み取り、OSキーチェーンからの資格情報取得 | 漏洩は不可逆。Read権限そのものをdenyし、Bashのsandbox側でもファイルシステムの読み取りを塞ぐ二重防御にする |
| ネットワークアクセス | curl・wget・ssh・nc・scp等の生の通信コマンド | コマンド自体をdenyしつつ、必要な通信は許可ドメインのリスト(allowlist)に寄せて経路を絞る |
| システム設定変更 | defaults write、scutil、nvram、csrutil、networksetup等のmacOSシステム設定変更 | セッションの外側にまで影響が漏れる操作。開発作業では基本的に不要なので広めにdenyしてよい |
この分類は、実際に半年以上運用してきたdotfilesのpermissions.denyから抜き出したものだ。中身は例えばこんな形になる(ワイルドカードの書き方は後述するので、ここではカテゴリのイメージだけ掴んでもらえれば十分)。
{
"permissions": {
"deny": [
// 破壊的操作: 取り返しがつかない
"Bash(rm -rf:*)",
"Bash(git push --force:*)",
"Bash(git push *:main)",
// 秘匿情報アクセス: 漏洩は不可逆
"Read(./.env)",
"Read(./.ssh/**)",
"Bash(security find-generic-password:*)",
// ネットワークアクセス: 経路をWebFetchやallowlistに寄せる
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(ssh:*)",
// システム設定変更: 開発作業では基本不要
"Bash(defaults write:*)",
"Bash(networksetup:*)",
"Bash(csrutil:*)"
]
}
}
ネットワークアクセスは「denyで塞ぐ」だけでなく「許可した経路だけ通す」allowlist側の設計も効く。sandbox機能を使っている場合、sandbox.network.allowedDomainsにレジストリやAPIのドメインを列挙しておけば、それ以外へのアクセスはコマンドの種類に関わらず構造的に失敗する。deny(禁止コマンドの列挙)とallowlist(許可ドメインの列挙)は、同じ「ネットワークアクセス」というリスクに対する二枚のフィルターだと考えるとわかりやすい。
allow・deny・ask の使い分けを決定木で持つ
deny rulesを書いているとよく迷うのが、「これはdenyで完全に止めるべきか、それともaskで人間の確認を挟むべきか」という判断だ。ここで効くのが、可逆性・影響範囲・頻度という3つの軸を使った決定木になる。
「可逆性が低い・影響範囲が広い」ものは迷わずdenyでいい。判断が割れるのは、その先の「静的なパターンマッチだけで判定できるか」という分岐だ。
コマンド文字列だけで白黒つけられるなら、deny(またはallow)で確定させてしまう。一方で、「環境変数の中身がprodっぽい値かどうか」のように、実行時の文脈を見ないと判断できない操作もある。こうしたケースにdenyで完全ブロックを当てると、正当な操作まで一律に弾いてしまい、結局「このルールだけ外して」という個別対応が積み重なっていく。
実際に踏んだ例として、prod環境のcredentialらしき文字列がBashコマンドに紛れていないかを検知するhookがある。環境変数名にPRODやLIVEを含む参照、.env.productionの読み込み、prodを指すAWSプロファイルでの呼び出し——これらを検知したとき、このhookは完全blockではなくhookSpecificOutput.permissionDecision = "ask"を返す設計になっている。検知ロジックが完璧でない以上、「怪しいから即block」にしてしまうと、たまたま変数名にPRODを含むだけの無害な操作まで止まってしまう。askにしておけば、誤検知のときに人間が「これは大丈夫」と一言確認するだけで先に進める(denyで全部止めて毎回設定ファイルを書き換えるより、よほど平和的な解決策だ)。
つまり判断基準はこう整理できる。
- 静的パターンで確定できる、かつ不可逆・広範囲 → deny
- 静的パターンで確定できる、かつ可逆・限定的 → allowでよい(denyを増やしすぎない)
- 実行時の文脈依存で、誤検知の害が大きい → ask(hookのpermissionDecision経由)
- 実行時の文脈依存だが、低頻度・限定的 → hookで個別に判定してdenyかaskを返す
「判定できないから全部deny」に倒すと運用が窮屈になり、「判定できないから全部allow」に倒すと事故る。askという第3の選択肢は、この両極端を避けるための逃げ道として使うものだ。
ワイルドカードパターンの落とし穴
denyルールはたいていBash(git push *:main)のようなワイルドカードで書く。ここで踏みやすい罠が3つある。
罠1: 前方一致だけの判定はオプションで迂回される
コマンドが「特定の文字列で始まっているか」だけを見る判定は、コマンドとサブコマンドの間に別のトークンが挟まると素通りする。実際に、push系コマンドを検知するhookの判定条件が"Bash(git push*)"という前方一致のままだったため、git -C <dir> push origin mainのようにgitとpushの間にグローバルオプション(-C、--git-dir等)が挟まるコマンドを検出できていなかった、という事例があった。素のgit push origin mainはdenyされる一方、-Cを1つ挟むだけでそのまま通っていた、というオチだ(正直、安全策のつもりで書いた1行が、抜け道の設計図も兼ねていたことになる)。
この手のバイパスをどう塞ぐかは、deny ruleの前方一致バイパスを検証した記事で詳しく扱っているので、ここでは深掘りしない。この記事の範囲で言えることは1つで、「文字列の前方一致」だけに頼った判定は、コマンドの間に何かが挟まった瞬間に破綻する、ということだけ覚えておけば十分だ。対処の方向性としては、hook側の起動条件(if条件)自体を広めに取り、判定ロジックの中身でトークンを1つずつ読んで確定させる、という形に倒すことになる。
罠2: 広すぎる部分一致は無害な操作まで巻き込む
逆に、判定を広げすぎると今度は無害な操作まで引っかかる。「コマンド文字列のどこかにpushという単語が含まれていたらask」という単純な走査ロジックを書いたところ、git stash pushやgit log --grep push、git commit -m pushのような、push先ブランチとは何の関係もない通常操作までaskに倒れてしまう、という事例があった。原因は、直前のトークンの種類を見ずに「後ろにpushという文字列があるかどうか」だけで判定していたことにある。
修正の方向性はシンプルで、「直前のトークンが未知のオプションらしきものである場合に限って走査する」というように、判定範囲を絞り込む条件を1つ足すだけで済む。ワイルドカードは広く書けば安全というわけではなく、広げすぎると別の意味で信頼を失う(安全のために確認を挟んでいるはずなのに、何でもかんでもask確認を求めてくると、そのうち確認ダイアログを脊髄反射でYesする癖がつく。それはそれで本末転倒だ)。
罠3: 列挙しきれない可変長引数は「ask」に逃がす
もう1つ、意外と見落としやすいのが可変長の引数だ。push系コマンドの保護ブランチ検知で、コマンド中の1箇所の引数位置だけを見ていたため、git push origin feat:feat dev:devのように複数の宛先を同時に指定するpushでは、2つめ以降の宛先がチェックされていなかった。さらに--allや--mirrorのように「送信先を列挙できない」オプションも、そもそもパターンマッチの対象にできていなかった。
この2つに対する落としどころは、「全部の引数を走査して1つでも保護対象に一致すればdeny」というのが基本だが、--all・--mirrorのように宛先を静的に列挙できないケースについては、無理にホワイトリスト的な判定をせず、askにフォールバックするという設計にした。これは前節の決定木そのままで、「静的パターンで確定できないなら、denyでもallowでもなくaskに逃がす」という判断がここでも効いている。
余談だが、この種の構造的な限界はClaude Code固有の話ではない。別のAIコーディングツールのpermission機構でも、「コマンドの先頭部分だけを見て一致させる」方式を使う場合、["git", "push"]というパターンはgit -C <path> push ...のようにトークンの並びが変わったコマンドには一致しない、という同じ注記が運用メモに残っている。文字列の前方一致だけに頼るパターンマッチには、ツールを問わず同じ構造的な壁がある、と考えておくと、Claude Code以外の環境でpermission設計をするときにも応用が利く。
安全に書くための3つの意識
- パターンは「動詞+典型的な引数」ではなく、「起こりうる呼び出し形をできるだけ列挙する」意識で書く。グローバルオプション経由の迂回を最初から想定しておく
- hookの起動条件(
if/matcher)は広めに取り、絞り込みは中身のロジックに任せる。起動条件を絞りすぎると、そこが新しい迂回経路になる - 静的パターンで判定しきれないと分かったら、無理にワイルドカードを頑張らず、素直にaskへ倒す。「判定できない」を認めるのも設計判断のうちだ
最小構成から段階的にルールを積み上げる
いきなり全カテゴリを網羅しようとすると手が止まる。実際に効率がいいのは、リスクの大きい順に段階を踏んで足していくやり方だ。
| フェーズ | やること | 理由 |
|---|---|---|
| Phase 0 | permissions.allowに標準ツール(Read/Write/Edit/Bash等)を許可し、denyはまだ空のまま | Claude Codeを一般的な用途で使うには不便がない状態から始める。「denyがなくて怖い」状態は一時的なもの |
| Phase 1 | 破壊的操作から着手(rm -rf、force push、保護ブランチへの直push) | 可逆性が最低・被害が最大のカテゴリを最優先で塞ぐ |
| Phase 2 | 秘匿情報アクセスをdeny(.env系のRead、キーチェーンアクセス) | 漏洩も不可逆。破壊的操作の次に優先度が高い |
| Phase 3 | ネットワークアクセスを制限(deny+allowlist) | 外部への副作用は影響範囲が読みにくい。ここまでで主要リスクはほぼ塞がる |
| Phase 4 | システム設定変更や、判断が必要な操作の洗い出し | 頻度が低く後回しにしやすい。判断が要るものはaskに倒す |
各フェーズを足すたびに、そのルールが実際に効いているかを確認する。確認方法は難しくなくて、settings.jsonにdenyルールを1行足したら、別セッションでそのコマンドをそのまま実行させてみればいい。ブロックされていればTool execution blockedのようなメッセージが返ってくるはずで、返ってこなければパターンの書き方を疑う。これを新しいdenyルールを足すたびに繰り返すだけで、「書いたつもりで効いていない」ルールを早い段階で見つけられる。
段階を踏まずに一気に書こうとすると、「これも危ない気がする」で手が止まってPhase 1にすら着手できない、というパターンに陥りやすい(allowを絞りすぎて自分の首を絞めるのも、だいたいこの段階で起きる)。まずはPhase 1だけ埋めて動かし、実際に使いながら次のフェーズを足していく方が、結果的に早く形になる。
まとめ
permissionsの設計は、思いついた順にルールを積んでいくと、いずれ今日見たような迂回パターンのどれかに引っかかる。リスクをカテゴリで整理し、deny・allow・askを可逆性・影響範囲・頻度で振り分け、ワイルドカードは「起こりうる呼び出し形を列挙する」意識で書く——この3つを最初の1行から一貫させておけば、後からルールが増えても迷わない。
あなたのプロジェクトのpermissions.denyがまだ数行しかないなら、今日がPhase 1を埋めるいい機会だ。逆にすでに大きく育っている場合も、今あるルールをこの4カテゴリに当てはめ直してみると、「これは何のために書いたんだっけ」というルールが1つや2つは見つかるはずだ。
Hooksとpermissionsの役割分担そのものを詳しく知りたい場合はClaude Code Hooks から Permissions への移行を、settings.json全体(hooks/effortLevel含む)の半年運用の落とし穴はsettings.jsonの落とし穴と落とし所を、それぞれ参照してほしい。



