Claude Code(AIコーディングエージェント)を実運用していると、必ず権限プロンプトにぶつかる。git status を打つたび、cargo build を走らせるたびに「実行していいですか?」と聞かれる。最初は安心だが、一日に何十回も許可していると限界が来る。
かといって全部素通しにすると、今度はクレデンシャルの漏洩が怖い。エージェントがうっかり ~/.aws/credentials を開いてチャットログに秘密を吐いたり、.env をコミットに混ぜたりしたら目も当てられない。
この記事は、その両方 —「日常のコマンドは聞かれず、危険なものは問答無用で止める」— を settings.json の allow/ask/deny で作った実録である。一日ハマって分かった落とし穴も全部書く。特に「どの settings.json が読まれるのか」は、図で見ないと本当に分かりにくい。
設定ファイルは3層 — そして「どこに置くか」で最初にハマる
Claude Codeの権限設定は3つのファイルに書ける。読み込み順があり、後が前を上書きする。

| ファイル | スコープ | 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を再起動しても駄目。原因はこれ:

Claude Codeが読むのは「起動したディレクトリの .claude/」だけで、サブディレクトリの .claude/ は見ない。私は複数プロジェクトが入った親フォルダで起動していたのに、許可を書いていたのは実際に作業しているサブプロジェクト側。永久に読まれないファイルを延々と編集していた。
見分け方は簡単で、コマンドが cd サブプロジェクト && から始まっていたら要注意。それはあなたがサブディレクトリで作業しているのに、Claude Codeは親で起動している証拠だ。設定は起動した親の .claude/ に置く。
allow / ask / deny — 3つのリストと優先順位
置き場所が分かったら中身。permissions には3つのリストがあり、deny > ask > allow の順で強い。

この優先順位が効いてくる。たとえば 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 *.env は deny にマッチするので落ちる。だから「普通のソースは聞かずに読む、秘密ファイルだけ絶対拒否」が両立する。これが3層の美しいところ。
限界 — そして PreToolUse フックで airtight に
正直に書くと、この deny パターンには穴がある。cat や Read は塞げても、こう来られると抜ける:
python3 -c "print(open('/Users/me/.aws/credentials').read())"
grep key ~/.aws/credentials
python3 も grep も allow なので、インタプリタや別コマンド経由の読み出しはすり抜ける。日常の事故(うっかり cat、うっかりReadツール)はこれで防げるが、「どんなコマンドで来ても絶対」にするには足りない。
airtight にするなら PreToolUse フックが答えだ。フックはコマンド文字列そのものを検査できるので、~/.aws/・.env・credentials といった文字列が含まれていたら、コマンドの中身が何であれ(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割はこれ |
日常コマンドを allow(node:* など広めに) |
手動許可の消耗から解放 |
cd も allow に入れる |
複合コマンドが通る |
秘密ファイルは ask でなく deny |
迷う余地をなくす |
cat と Readツールの両方を deny |
「開く」経路を両方塞ぐ |
AWS/GCP/ssh/.pem/.env を網羅 |
二大クラウド鍵をカバー |
env/git add も deny |
ダンプ・誤コミットの経路 |
| 網羅性が要るなら PreToolUseフック | インタプリタ経由も止める |
AIエージェントに仕事を任せるほど、「何を許し、何を絶対に止めるか」の設計が効いてくる。allow で自分を楽にし、deny で自分を守る。その境界を settings.json で引く — それが実運用の第一歩だった。


コメント