Claude Codeの権限設定でハマらないために — settings.jsonの適用範囲と、クレデンシャル漏洩をDenyで止める多層防御

AI

Claude Code(AIコーディングエージェント)を実運用していると、必ず権限プロンプトにぶつかる。git status を打つたび、cargo build を走らせるたびに「実行していいですか?」と聞かれる。最初は安心だが、一日に何十回も許可していると限界が来る。

かといって全部素通しにすると、今度はクレデンシャルの漏洩が怖い。エージェントがうっかり ~/.aws/credentials を開いてチャットログに秘密を吐いたり、.env をコミットに混ぜたりしたら目も当てられない。

この記事は、その両方 —「日常のコマンドは聞かれず、危険なものは問答無用で止める」— を settings.jsonallow/ask/deny で作った実録である。一日ハマって分かった落とし穴も全部書く。特に「どの settings.json が読まれるのか」は、図で見ないと本当に分かりにくい。

設定ファイルは3層 — そして「どこに置くか」で最初にハマる

Claude Codeの権限設定は3つのファイルに書ける。読み込み順があり、後が前を上書きする。

図1

ファイル スコープ git 用途
~/.claude/settings.json 全プロジェクト 個人の共通設定
.claude/settings.json プロジェクト commit チーム共有のルール
.claude/settings.local.json プロジェクト gitignore 自分だけの上書き

sshの鍵パスやマシン固有の許可は個人情報なので settings.local.json(gitignore済み)に、rm -rf 禁止のような全プロジェクト共通の安全策は ~/.claude/settings.json(グローバル)に置く。この使い分けが基本。

最大の落とし穴 —「起動ディレクトリ」を勘違いする

上の表の「起動ディレクトリ」がクセモノだった。私は settings.local.json に許可を書きまくったのに、まったく効かなかった。GUIを再起動しても駄目。原因はこれ:

図2

Claude Codeが読むのは「起動したディレクトリ.claude/」だけで、サブディレクトリの .claude/ は見ない。私は複数プロジェクトが入った親フォルダで起動していたのに、許可を書いていたのは実際に作業しているサブプロジェクト側。永久に読まれないファイルを延々と編集していた。

見分け方は簡単で、コマンドが cd サブプロジェクト && から始まっていたら要注意。それはあなたがサブディレクトリで作業しているのに、Claude Codeは親で起動している証拠だ。設定は起動した親の .claude/ に置く。

allow / ask / deny — 3つのリストと優先順位

置き場所が分かったら中身。permissions には3つのリストがあり、deny > ask > allow の順で強い。

図3

この優先順位が効いてくる。たとえば cat を広く allow しつつ、cat *.env だけ deny に入れれば、普通のファイルは聞かずに読むが、秘密ファイルだけ問答無用で落ちる。同じコマンドでも引数で運命が分かれる。

実践1:日常の開発コマンドをallowに

まずは「聞かれると鬱陶しいが安全なもの」を allow に。私のRust + WASMプロジェクトだとこうなった(抜粋):

{
  "permissions": {
    "allow": [
      "Bash(cargo build:*)", "Bash(cargo test:*)", "Bash(cargo run:*)",
      "Bash(cargo clippy:*)", "Bash(cargo fmt:*)",
      "Bash(git status:*)", "Bash(git log:*)", "Bash(git diff:*)",
      "Bash(git add:*)", "Bash(git commit:*)", "Bash(git push:*)",
      "Bash(gh pr:*)", "Bash(gh api:*)",
      "Bash(node:*)", "Bash(npx:*)",
      "Bash(cd:*)", "Bash(ls:*)", "Bash(cat:*)", "Bash(grep:*)",
      "Bash(head:*)", "Bash(find:*)", "Bash(sed -n:*)",
      "Bash(ssh:*)", "Bash(scp:*)"
    ]
  }
}

パターンは Bash(コマンド:*) で「そのコマンドを任意の引数で」を意味する。node:* を入れておくと、新しいテストスクリプト(node tools/webtest/新しいの.mjs)を作っても聞かれない — これが地味に効く。特定スクリプト名を1個ずつ許可していると、ファイルを作るたびに聞かれてキリがない。

落とし穴 —「複合コマンドは全部の部分がallowでないと通らない」

cd rustx86 && git status && git log がなぜか聞かれる。原因は複合コマンドだ。許可判定は && で区切った各コマンドを1つずつ照合し、全部が許可対象でないと自動承認しない。私のリストには git status はあったが cd が無かった。だから引っかかっていた。Bash(cd:*) を足したら通った。

もう一つの落とし穴 — ホットリロードと再起動

設定を書いても即効くとは限らない。ファイルは基本起動時に読まれる。ただし正しい場所(起動ディレクトリの .claude/)に書けば、多くの場合ホットリロードされて再起動不要で効く。逆に、いくら再起動しても効かないなら、それは「効かない場所に書いている」サインだ(前述のサブディレクトリ問題)。

実践2:クレデンシャル漏洩を「問答無用で」止める

ここが本題。最初は「秘密ファイルを開くときは ask(確認)でいいか」と思ったが、考え直した。ASKは弱い。確認ダイアログは「うっかりYes」を押せてしまうし、そもそも ~/.aws/credentials開こうとした時点でアウトであるべきだ。迷う余地はない。だから全部 deny に倒す。

{
  "permissions": {
    "deny": [
      "Bash(cat *credential*)", "Bash(cat *secret*)",
      "Bash(cat *.env)", "Bash(cat *.pem)", "Bash(cat *.key)",
      "Bash(cat *service-account*)",
      "Bash(cat ~/.aws/*)", "Bash(cat ~/.config/gcloud/*)",
      "Bash(cat ~/.ssh/*)", "Bash(cat ~/.netrc)", "Bash(cat ~/.npmrc)",
      "Bash(env)", "Bash(printenv:*)",
      "Bash(git add *.env)", "Bash(git add *.pem)", "Bash(git add *credential*)",
      "Bash(rm -rf:*)", "Bash(rm -fr:*)", "Bash(sudo rm:*)",

      "Read(//Users/me/.aws/**)",
      "Read(//Users/me/.config/gcloud/**)",
      "Read(//Users/me/.ssh/**)",
      "Read(**/*.pem)", "Read(**/.env)", "Read(**/credentials)"
    ]
  }
}

ポイントを3つ。

cat だけでなく Read ツールも塞ぐ。 エージェントがファイルを「開く」経路は、Bashの cat と、エージェント自身のReadツールの2つある。前者だけ塞いでも、Readツールで開かれたら意味がない。だから Read(//Users/me/.aws/**) のようにツールレベルでも拒否する。「開こうとした時点でアウト」を両方の経路で徹底する。

② AWSもGCPも入れる。 ~/.aws/credentials(AWS)と ~/.config/gcloud/(GCP application default credentials)は現代の開発機の二大クラウド鍵。忘れずに。加えて .pem .key service-account*.json のようなファイル名パターンでも拾う(置き場所によらず捕まえる)。

③ 漏洩の別経路も塞ぐ。 env / printenv は環境変数を全ダンプするので、AWS_SECRET_ACCESS_KEY 等がチャットに丸見えになる漏洩経路だ。deny する。git add *.env のような誤コミットの直接パターンも一応入れておく(.gitignore が主防御だが、念入りに)。

deny と allow が両方マッチしたら?

さっきの優先順位図の通りdeny が勝つcat:*allow に入れていても、cat *.envdeny にマッチするので落ちる。だから「普通のソースは聞かずに読む、秘密ファイルだけ絶対拒否」が両立する。これが3層の美しいところ。

限界 — そして PreToolUse フックで airtight に

正直に書くと、この deny パターンにはがある。catRead は塞げても、こう来られると抜ける:

python3 -c "print(open('/Users/me/.aws/credentials').read())"
grep key ~/.aws/credentials

python3grepallow なので、インタプリタや別コマンド経由の読み出しはすり抜ける。日常の事故(うっかり cat、うっかりReadツール)はこれで防げるが、「どんなコマンドで来ても絶対」にするには足りない。

airtight にするなら PreToolUse フックが答えだ。フックはコマンド文字列そのものを検査できるので、~/.aws/.envcredentials といった文字列が含まれていたら、コマンドの中身が何であれ(python3 だろうが grep だろうが)ブロックできる。同じフックで「git commit 前にステージされたファイルをスキャンして、秘密が混ざっていたら止める」も作れる — これが git add . で誤って秘密を巻き込むケースまで拾う、誤コミットへの本当の答えになる。

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.command' | grep -qE '\\.aws/|\\.env|credentials|\\.pem' && echo '{\"decision\":\"block\",\"reason\":\"クレデンシャル参照を検出\"}' || true"
      }]
    }]
  }
}

(※これは骨子。実運用ではパターンを詰めて、誤検知しすぎないよう調整する。)

deny パターンを第1層(宣言的・軽い)、フックを第2層(手続き的・網羅的)に置くのが現実的な多層防御だ。

まとめ

やったこと 効果
設定は起動ディレクトリの .claude/ に置く 「効かない」の9割はこれ
日常コマンドを allownode:* など広めに) 手動許可の消耗から解放
cd も allow に入れる 複合コマンドが通る
秘密ファイルは ask でなく deny 迷う余地をなくす
catReadツールの両方を deny 「開く」経路を両方塞ぐ
AWS/GCP/ssh/.pem/.env を網羅 二大クラウド鍵をカバー
env/git add も deny ダンプ・誤コミットの経路
網羅性が要るなら PreToolUseフック インタプリタ経由も止める

AIエージェントに仕事を任せるほど、「何を許し、何を絶対に止めるか」の設計が効いてくる。allow で自分を楽にし、deny で自分を守る。その境界を settings.json で引く — それが実運用の第一歩だった。

コメント

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