🔐

Claude Code をチームに配るときの settings.json — deny / ask / allow をどう引いたか

に公開






CLI を使わないメンバーを含むチームに Claude Code を配ることになり、配布用の settings.json を設計しました。この記事は、その設定を deny / ask / allow のどこに何を置いたか、なぜそうしたかを根拠ごと公開するものです。

対象は、チームに Claude Code を配る側の人。情シスがいない小規模な組織を想定しています。持ち帰ってほしいのは設定のコピーではなく、「なぜ allow ではなく ask なのか」を自分で説明できる状態です。

プロンプトの書き方、コード品質、エージェント設計は扱いません。「配って壊れないようにする」までが範囲です。

先に、いちばん大事な前提

設定の話に入る前に、ひとつはっきりさせておきます。

ユーザーに settings.json を配る方式は、強制ではありません。

Claude Code の設定ファイルにはスコープがあり、優先順位が決まっています。

スコープ 場所 優先度
Managed 管理者が配信(MDM / OS ポリシー / server-managed settings 等) 最高。他のどのスコープからも上書きできない
Local .claude/settings.local.json 2
Project .claude/settings.json 3
User ~/.claude/settings.json 最低

配布用ファイルを各自の ~/.claude/settings.json に置いてもらう方式は、いちばん優先度の低いスコープに置くということです。本人がエディタで開いて数行消せば、それで終わります。

これは「意味がない」という話ではありません。悪意のない事故を防ぐ用途では十分に機能します。 実際、私たちが想定していたのは攻撃者ではなく、「よくわからないまま Enter を押してしまう」状況でした。そこには効きます。

ただし、本当に強制したいなら managed settings を使う必要があります。 組織的に配れる環境があるなら、最初からそちらを検討してください。この記事の設定内容はそのまま managed settings にも置けます。

なお permission rules だけは他の設定と挙動が違い、スコープをまたいで「上書き」ではなく「マージ」されます。 そして deny はどのスコープのものでも allow より先に評価されます。ユーザー設定の deny がプロジェクト設定の allow を打ち消す、という向きも成り立ちます。

評価順序を先に押さえる

設定を読む前にこれだけは必要です。ルールは deny → ask → allow の順に評価され、最初にマッチしたものが結果を決めます。ルールの詳しさ(specificity)は順序に影響しません。

つまり、こう書いても例外になりません。

{
  "permissions": {
    "deny": ["Bash(aws *)"],
    "allow": ["Bash(aws s3 ls)"]
  }
}

aws s3 ls は deny 側に先にマッチするのでブロックされます。deny に例外を持たせることはできない。 「基本は禁止、これだけ許可」を deny で書こうとすると必ず失敗します。

配布した設定の骨格

全文はこの記事では出しません(後述の理由による)。骨格はこうです。

{
  "permissions": {
    "defaultMode": "default",
    "deny": [
      "mcp__*",
      "Read(~/.ssh/**)",
      "Read(~/.aws/**)"
    ],
    "ask": [
      "Bash(curl *)",
      "Bash(wget *)",
      "Bash(npx *)"
    ],
    "disableBypassPermissionsMode": "disable"
  }
}

以下、1 ブロックずつ根拠を書きます。

なぜ「防止」ではなく「封じ込め」なのか

設計方針を先に置きます。想定した本命の脅威は、間接プロンプトインジェクションです。

Claude が読んだ Web ページやファイルの中に「以下の指示に従え」と書いてあり、それをモデルが指示として扱ってしまう類の攻撃です。公式ドキュメントも安全対策を列挙したうえで、こう書いています。

While these protections significantly reduce risk, no system is completely immune to all attacks.

これを読んだうえで、入力を検閲して「防止」する方向には倒しませんでした。 外部から読み込む内容を全部信頼できない前提に立つと、入口を塞ぐ設計はどこかで破綻します。読ませるものを減らせば、そもそも道具として使えません。

代わりに置いたのは「騙されても、できることが限られていればいい」という考え方です。危険なのはモデルが騙されること自体ではなく、騙されたモデルが強い権限を持っていることです。だから権限側を絞ります。

絞る対象は 3 つに分けて考えました。

  1. 読める範囲 — 何を読まれると困るか
  2. 書ける範囲 — 何を書き換えられると困るか
  3. 外に出せる範囲 — 何を送信されると困るか

このうち 3 がいちばん優先度が高いと判断しました。読まれるだけなら被害は手元で止まりますが、送信されると取り返しがつきません。以降の設定はこの順で効かせています。

deny に置いたもの

deny は「例外なく通さない」枠です。例外を持たせられない以上、ここに置くのは一切許可する気がないものだけになります。

MCP を mcp__* で一括 deny する

"deny": ["mcp__*"]

deny / ask ルールはツール名の位置にグロブを書けます。mcp__*全サーバーの全 MCP ツールにマッチします。

一括 deny にした理由は、MCP が増えるものだからです。個別に「これは危ない」と潰していく方式は、新しいコネクタが増えるたびに設定を追う必要があり、追い忘れた瞬間に穴になります。既定で全部止めて、必要なものだけ足す方が、放置したときの壊れ方が安全です。

ここで注意点があります。mcp__* を allow 側に書いて許可を戻すことはできません。

An unanchored allow glob such as "*", "B*", or "mcp__*" is skipped with a warning and doesn't auto-approve anything.

allow でグロブが使えるのは、mcp__<サーバー名>__ という具体的なサーバー名までを literal で書いた後ろだけです。mcp__github__get_* は有効ですが、mcp__* は警告付きで無視されます。サーバー名を書かせることで「どのサーバーを信頼したのか」を設定上に明示させる設計になっています。

bypassPermissions を封じる

"disableBypassPermissionsMode": "disable"

bypassPermissions は権限確認をまるごと飛ばすモードです。公式の説明も踏み込んでいます。

bypassPermissions offers no protection against prompt injection or unintended actions.

ここまでの設計を全部無効化できるスイッチが手元に残っている状態は避けたいので、封じました。値は文字列 "disable" です。

ただし前述のとおり、ユーザー設定に置いたこれは本人が消せます。 公式も「managed settings で使うのがいちばん有効」と明記しています。

These are most useful in managed settings where they can't be overridden.

同じ形で disableAutoMode もあります。auto モードについては、当初「無効化する」案で書いていましたが、最終的に各自の裁量で使ってよいに変えました。理由は本の方で書きます。

ask に置いたもの

ここがこの記事でいちばん伝えたい部分です。

"ask": [
  "Bash(curl *)",
  "Bash(wget *)",
  "Bash(npx *)"
]

「ask なんて要るのか」への答え

先に事実を整理します。curlwget は、何も設定しなくても既定で確認が入ります。

Commands that fetch content from the web such as curl and wget are not auto-approved by default. They prompt like any other non-read-only Bash command.

では明示的に ask ルールを書く意味は何か。モードを貫通するからです。

権限モードには auto(分類器が裏で判定して確認を省く)や bypassPermissions(全部飛ばす)があります。これらのモードでも、明示的な ask ルールだけは確認を強制します。

Explicit ask rules and connector tools your organization set to ask still force a prompt in this mode.

つまり ask ルールは「既定の挙動をなぞっただけの無駄な行」ではなく、モードが緩められても外れない床です。auto モードを各自の裁量に委ねる判断ができたのは、この床があったからです。

なぜ allow ではないのか

これらを allow に入れると、「外に出せる範囲」が無確認で開きます。 さらに allow はモードを貫通しません。

Allow rules have no effect in bypassPermissions because everything else is already approved.

そして auto モードに入ると、広すぎる allow ルールは自動的に落とされます。Bash(*)Bash(python*) のような任意コード実行を与えるルール、パッケージマネージャの run コマンド、Agent の allow ルールが対象です。Bash(npm test) のような狭いルールは残ります。

allow を厚くする設計は、モードが変わると意味が変わるということです。ask を厚くする設計にはその不安定さがありません。

ask を増やしすぎない

とはいえ ask を増やすほど確認が増え、「とりあえず全部 Enter」に退化します。 これは設定の問題ではなく人間の問題なので、設定側では解けません。

線引きは「外に出る経路かどうか」で引きました。curl / wget は明確に外向き。npx はレジストリから任意のコードを取ってきて実行するので、外向きと実行の両方に該当します。逆に、ローカルで閉じる操作は ask に入れていません。

認証情報のパスを守る — ここがいちばん間違えやすい

案件で預かった鍵やクラウドの認証情報がローカルにある前提で、読み取り経路を deny しました。そしてここで 2 回間違えました。

間違い 1: 先頭のスラッシュは絶対パスではない

Read / Edit ルールのパス指定は gitignore 記法で、4 種類のパターンがあります。

書き方 意味
//path ファイルシステムのルートからの絶対パス
~/path ホームディレクトリから
/path 設定ファイルの置き場所からの相対
path / ./path カレントディレクトリから

3 番目が罠です。公式にも警告として書かれています。

A pattern like /Users/alice/file isn't an absolute path. The single leading slash anchors at the settings source, not the filesystem root.

さらに、ユーザー設定に書いた場合の解決先が明示されています。

A deny rule such as Read(/secrets/**) in user settings blocks ~/.claude/secrets/**, not a secrets directory in your project.

つまり ~/.claude/settings.jsonRead(/Users/foo/.ssh/**) と書くと、それは ~/.claude/Users/foo/.ssh/** を指します。存在しないパスを守っている状態で、本人は守った気になります。エラーも出ません。

正しくはスラッシュ 2 本の Read(//Users/foo/.ssh/**)、またはホーム相対の Read(~/.ssh/**) です。

間違い 2: deny は「Claude の道具」にしか効かない

こちらの方が重大です。

Read and Edit deny rules apply to Claude's built-in file tools and to file commands Claude Code recognizes in Bash, such as cat, head, tail, and sed. They don't apply to arbitrary subprocesses that read or write files indirectly, like a Python or Node script that opens files itself.

Read の deny が効くのは、組み込みのファイルツールと、Claude Code が認識できる cat / head / tail / sed などです。Python や Node のスクリプトがファイルを開く経路には効きません。

deny ルールで鍵ファイルを守ったつもりでも、スクリプトを 1 本書かれたら読めます。OS レベルで止めたいなら、公式の案内どおりサンドボックスを有効にする必要があります。

For OS-level enforcement that blocks all processes from accessing a path, enable the sandbox.

この 2 つを知らずに書いた deny ルールは、書いた本人だけが守られたと思っている行になります。私はなりました。

ついでに: 効かないルールを書いても静かに無視される

Write(path)Glob(path) のようなパス付きルールは、受理はされますが参照されません。 ファイルのパーミッションは Edit(path)Read(path) だけで判定されます。起動時に警告は出ますが、見落とせば終わりです。

Permission deny rule (.claude/settings.json): Write(docs/**) is not matched by
file permission checks — only Edit(path) rules are. Use Edit(docs/**) instead

Write(...) ではなく Edit(...) を使ってください。

よく見る設定例の、ここが危ない

紹介記事の設定例でよく見かける型を 3 つ挙げます。特定の記事を指しているわけではなく、頻出する形の話です。

1. Bash(curl*) とアスタリスクの位置

スペースの有無で意味が変わります。

The space before * matters: Bash(ls *) matches ls -la but not lsof, while Bash(ls*) matches both.

Bash(curl*)curl で始まる別コマンドにも巻き添えでマッチします。逆に allow 側でこれをやると、意図しないコマンドまで許可されます。末尾に限っては Bash(ls:*)Bash(ls *) と同義ですが、Bash(git:* push) のように途中に書いた : はただの文字として扱われ、マッチしません。

2. allow と deny の二値で組まれている

ask が無い設定は、確認して通すという中間の選択肢を捨てています。運用すると必ず「毎回止まって面倒」になり、allow が肥大していきます。 前述のとおり allow はモードを貫通しないので、太らせるほど設計が脆くなります。

3. 認証情報の deny パスが効いていない

前節のとおりです。書いてあること自体が安心材料にならないので、自分の設定を読み返すときは「このパターンはどこを指しているか」を 1 行ずつ確認してください。

検証環境

  • 記事中の仕様の記述は、執筆時点の公式ドキュメントで裏取りしていますPermissions / Permission modes / Settings / Security
  • 対象は macOS のデスクトップアプリ利用を前提としています。Windows は未検証です
  • Claude Code の権限まわりは変更が速い領域です。設定を投入する前に、必ず自分の環境とバージョンで挙動を確認してください

この記事で扱えていないこと

この記事は「設定の考え方」に絞りました。実際に配って運用するには、これだけでは足りません。

  • 非エンジニアへの配布手順 — 隠しフォルダに置いてもらう手順書をどう書くか。ターミナルを触らせずに完結させる方法
  • 運用ルール — 設定ファイルでは表現できず、チームの合意にするしかなかったもの
  • 案件フォルダ単位のガード — プロジェクト別 .claude/settings.json の使い分けと、ユーザー設定との競合
  • 配布用テンプレートの全文と、導入後の点検チェックリスト

これらは同じ一次体験からまとめた本の方で扱っています。この記事の内容だけでも設定の設計はできますので、必要になったら参照してください。


最後にもう一度だけ。設定を書いたら、意図したとおりに拒否されることを自分の手で確認してください。 この記事で挙げた 2 つの間違いは、どちらも「書いたのに効いていない」種類のもので、確認しない限り気づけません。

GitHubで編集を提案

Discussion