はじめに / 対象と前提
settings.json に permissions を書いて「これでもう聞かれないはず」にしたのに、まだ確認ダイアログが出る。逆に、deny したつもりのコマンドが普通に走る。自分はこの両方を踏んだ。
-
想定読者: Claude Code を日常的に使っていて、許可ルールを
settings.jsonに書いて共有したい人 - 環境: Claude Code v2.1.2xx 系(2026-09 時点で確認)、macOS / Linux
-
前提知識: JSON が読める、
.claude/settings.jsonの場所を知っている
厄介なのは構文として正しいのに意図どおり効かないケースで、原因が別々の仕様に散らばっていて追いづらい。実際にハマった3つに絞って、再現条件と回避策を書く。
TL;DR
- 評価順は deny → ask → allow の固定順。最初にマッチしたものが勝つので、
allowでdenyに例外を開けることはできない -
Write(docs/**)やGlob(src/**)は受理されるが参照されない。パス指定のルールはEdit()とRead()の2つだけ -
/src/**の先頭スラッシュはファイルシステムのルートではない。絶対パスは//で始める
手順 / 動かし方
最小構成。プロジェクト共有なら .claude/settings.json、自分だけなら .claude/settings.local.json に置く。
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(git commit *)"
],
"deny": [
"Bash(git push *)",
"Read(.env)"
],
"ask": [
"Bash(git clean *)"
]
}
}
ルールの形は Tool または Tool(指定子)。括弧なしの Bash は全 Bash コマンドにマッチする。
書いたら必ず確認する。/permissions を叩くと適用中のルール一覧とどのファイル由来かが出る。ここに出てこないルールは効いていない。ターミナルからは claude doctor で解決結果と無効なルールが見られる。
設定ファイルの優先順位は、マネージド設定 > claude --settings <file> > .claude/settings.local.json > .claude/settings.json > ~/.claude/settings.json の順で上が強い。
ハマりどころ1: allow は deny の例外にならない
aws は広く止めつつ一部だけ通したい、をやろうとして詰まった。
{
"permissions": {
"deny": ["Bash(aws *)"],
"allow": ["Bash(aws s3 ls)"]
}
}
aws s3 ls は実行できない。 ルールは deny → ask → allow の順に評価され、最初にマッチしたものが結果を決める。ルールの具体性は考慮されない。より狭い allow を書いても広い deny に先に捕まって終わり。ask と allow の間も同じで、マッチする ask があれば具体的な allow があってもプロンプトは出る。
回避策: 例外を作りたいなら deny を広く書かない。止めたいものだけを列挙する。
{
"permissions": {
"deny": ["Bash(aws s3 rm *)", "Bash(aws iam *)"],
"allow": ["Bash(aws s3 ls *)"]
}
}
なお deny に括弧なしのツール名(Bash だけ)を書くと、そのツールは Claude のコンテキストから丸ごと消える。括弧つきの Bash(rm *) はツールを残したままマッチした呼び出しだけ止める。挙動が違う。
ハマりどころ2: Write() と Glob() のパスルールは読まれない
「docs/ 以下だけ書き込みを許したい」で最初にこう書いた。
{
"permissions": {
"allow": ["Write(docs/**)"]
}
}
これは無視される。 ファイルのパーミッションチェックで参照されるのは Edit(path) と Read(path) だけ。Write / NotebookEdit / Glob / 旧 MultiEdit にパスを付けたルールは受理されるものの一度も参照されない。v2.1.210 以降なら起動時に警告が出るが、読み飛ばすと気づけない。
回避策: 対応はこう。
| 書きがちな形 | 正しい形 |
|---|---|
Write(docs/**) |
Edit(docs/**) |
NotebookEdit(docs/**) |
Edit(docs/**) |
Glob(src/**) |
Read(src/**) |
Read の deny は同じパスへの Edit / Write もブロックする(新規作成含む)が NotebookEdit はカバーされない。絶対に触らせたくないパスには Edit の deny も並べておく。
ハマりどころ3: 先頭 / は「ルート」ではない
~/.claude/settings.json にこう書いた。
{
"permissions": {
"deny": ["Read(/secrets/**)"]
}
}
プロジェクトの secrets/ は一切ブロックされない。 Read / Edit のパターンは gitignore 記法で、先頭スラッシュはルールを定義した設定ファイルのアンカーを指す。
| パターン | 意味 |
|---|---|
//path |
ファイルシステムのルートからの絶対パス |
~/path |
ホームディレクトリ基準 |
/path |
設定ファイルの由来元からの相対 |
path / ./path
|
カレントディレクトリからの相対 |
/path の解決先は置き場所で変わる。プロジェクト設定なら主作業ディレクトリ配下だが、ユーザー設定に書いた /secrets/** は ~/.claude/secrets/** に解決される。効かなかったのはこれが理由だった。
回避策: ユーザー設定から全プロジェクトに効かせるなら // の絶対パスか ~/ のホーム相対を使う。gitignore 準拠なので Read(.env) は Read(**/.env) と等価。
{
"permissions": {
"deny": ["Read(.env)", "Read(//**/.env)"]
}
}
背景・補足: Bash ルールはセキュリティ境界ではない
Bash のルールが見ているのはClaude が書いたコマンド文字列であって、プロセスではない。書き方が変われば素通りする。
| ルール | 止まる | 止まらない |
|---|---|---|
Bash(rm *) |
rm -rf build/ |
/bin/rm -rf build/、bash -c 'rm -rf build/'
|
Bash(git push *) |
git push origin main |
git -C . push origin main |
ラッパーは固定リストで剥がされる。timeout nice nohup などは剥がされるので Bash(npm test *) が timeout 30 npm test にマッチする。が、npx や docker exec のような実行系ランナーはリストにない。 Bash(devbox run *) は devbox run rm -rf . まで許すので、ランナーは Bash(devbox run npm test) と内側まで含めて書く。
文字列に依存しない強制力が要るなら、ルールではなくサンドボックスか PreToolUse hook を使う。ルールは「事故防止」、サンドボックスは「境界」と役割を分けるのが正しい。
まとめ
- ルールは deny → ask → allow の固定順で先勝ち。allow で deny に穴は開けられないので、deny は最初から狭く書く
- パス指定は
Edit()とRead()だけが読まれる。Write()/Glob()にパスを付けても参照されない - 先頭
/は設定ファイル基準のアンカー。絶対パスは//、ホームは~/、任意の深さはベア名か**/ - Bash ルールはコマンド文字列マッチなので
/bin/rmやsh -cでは回避される。境界が必要ならサンドボックスか hook - 書いたら
/permissionsとclaude doctorで適用されているかを必ず目で見る