Claude Codeの権限設計は「モードを選ぶ」だけでは終わらない ── settings階層・ルール優先順位・denyの限界を実機で確認する

Claude Codeの権限設計は「モードを選ぶ」だけでは終わらない ── settings階層・ルール優先順位・denyの限界を実機で確認する Claude Code活用

はじめに

Claude Codeには権限モード(default/acceptEdits/plan/auto/dontAsk/bypassPermissions)、permissions.allow/ask/denyルール、そして5段階の設定ファイル階層(managed → CLI --settings → プロジェクトlocal → プロジェクト共有 → ユーザー)がある。個人で使う分にはどれか1つを覚えておけば足りるが、チーム・企業で導入するとなると話が変わる。「プロジェクトの.claude/settings.jsonにdefaultMode: "auto"を書けば全員そのモードで始まるはずだ」という素朴な想定が、実際には静かに外れることがある。

この記事では、権限モード・設定階層・ルールの3つを1本の企業導入フローとして組み立て直し、公式ドキュメントが明記する「一部の設定ファイルではauto/bypassPermissionsが効かない」という仕様を、使い捨てのGitリポジトリでclaude -pを6回実行して実際に確認する。あわせて、「denyルールはallowルールに常に勝つ」「denyルールは文字列一致であって鉄壁ではない」という2つの実務上重要な性質も、実際に成功・失敗する様子を生の実行結果で示す。検証には実費約0.34ドルかかっている。

権限設計の全体像と検証の手順:3層が独立に効く

公式ドキュメント(Settings files and precedence)は、設定ファイルの優先順位を次の5段階と明記している(数字が小さいほど強い)。

  1. Managed settings(managed-settings.json、MDM、または claude.ai の管理コンソール) ── 組織
  2. Command line(claude --settings) ── そのセッションのみ
  3. Project local(.claude/settings.local.json) ── そのプロジェクトでの自分専用
  4. Shared project(.claude/settings.json) ── プロジェクトの全員
  5. User(~/.claude/settings.json) ── 自分の全プロジェクト

ここに、Permission modesドキュメントが定める権限モードと、Permissionsドキュメントが定めるallow/ask/denyルールが重なる。3つとも別々の軸で、それぞれ独立に「効くか効かないか」の条件を持っている。企業導入の勘所は、この3層を「個人の好み(モード)→チームの既定値(共有設定+ルール)→組織の強制(managed設定)」という順で積み上げることだが、その積み上げ方を間違えると、設定したつもりの制御が発動しないまま無人パイプラインが動き続ける。

実機検証1: defaultMode: "auto"は書く場所によって静かに無効化される

公式ドキュメントは次のように明記している(原文引用):

If you set "auto" in .claude/settings.json or .claude/settings.local.json, the value doesn’t take effect, and Claude Code then uses the built-in default rather than a defaultMode from ~/.claude/settings.json. If you set "bypassPermissions" in those two files, it doesn’t take effect either, and the session starts in Manual mode. The other values apply from any settings file.

つまり「プロジェクト共有設定」と「プロジェクトlocal設定」の2箇所に限り、autoとbypassPermissionsは無視される。これを実際に確かめる。

使い捨てのGitリポジトリを作り、.claude/settings.json(プロジェクト共有設定、Shared project)にdefaultMode: "auto"を書いた状態でclaude -pを実行し、作業ディレクトリ内へのファイル書き込みを依頼した。

rm -rf /tmp/perm-test && mkdir -p /tmp/perm-test/.claude && cd /tmp/perm-test
git init -q && git config user.email test@test.com && git config user.name test
cat > .claude/settings.json << 'EOF'
{
  "permissions": {
    "defaultMode": "auto"
  }
}
EOF
git add -A && git commit -q -m init

claude -p "proof.txtというファイルを作成し、中身に「ok」とだけ書いてください。確認や質問はせず、すぐに実行してください。" \
  --output-format json > out.json
grep -o '"result":"[^"]*"' out.json
grep -o '"permission_denials":\[[^]]*\]' out.json
ls proof.txt 2>&1 || echo "proof.txt NOT CREATED"

期待結果: autoモードが有効なら、作業ディレクトリ内へのローカルファイル書き込みは(公式ドキュメントの分類で「Allowed by default」)確認なしで成功するはずである。

実際の結果(2026-09-13、v2.1.268で実行):

result: "ファイル書き込みの権限が拒否されました。許可いただければ proof.txt を作成します。"
permission_denials: [{"tool_name":"Write","tool_use_id":"toolu_019YsYX4Lmf7EqVDmVmQTW2e","tool_input":{"file_path":"/private/tmp/perm-test/proof.txt","content":"ok"}}]
proof.txt NOT CREATED

autoモードなら通るはずの単純な書き込みが、permission_denialsに記録された上で拒否され、ファイルも作られなかった。これは「セッションが実際にはdefault(Manual)モードで動いていた」ことの動かぬ証拠になる(non-interactiveの-pでは、Manualモードで確認が必要な操作は、確認できる相手がいないため拒否されて終わる)。

成功判定: 同じ設定・同じ依頼を、今度は--settings(優先順位2位、CLI引数)経由で渡すとどうなるかを比べる。

cd /tmp/perm-test && rm -f proof.txt
claude -p "proof.txtというファイルを作成し、中身に「ok」とだけ書いてください。確認や質問はせず、すぐに実行してください。" \
  --settings '{"permissions":{"defaultMode":"auto"}}' \
  --output-format json > out2.json
grep -o '"result":"[^"]*"' out2.json
grep -o '"total_cost_usd":[0-9.]*' out2.json
ls -la proof.txt

実際の結果:

result: "proof.txtを作成し、中身に「ok」と書きました。"
total_cost_usd:0.112718
-rw-r--r-- 1 harkingbee wheel 2 proof.txt

.claude/settings.json(プロジェクト共有)では無視された同じ値defaultMode: "auto"が、--settings経由(CLI引数)だと即座に効き、実際にファイルが作られた。ドキュメントの記述通り、設定を書く「場所」によって同じ値の扱いが変わることを、否定・肯定の両方の生の実行結果で確認できた。

つまずき所として重要なのは、この無効化がエラーも警告も出さずに起きる点だ。プロジェクトの.claude/settings.jsonをgitにコミットして「これでチーム全員がautoモードで動く」と思っていても、CIやオンボーディングされたばかりのメンバーは黙ってdefault(Manual)モードのままになり、無人実行のパイプラインでは書き込みが拒否され続ける。この現象自体はGitHub Issue #91296(open、.claude/settings.local.jsonでのbypassPermissionsが無視される報告)・#86478(open、bypassPermissionsと--permission-modeフラグが無視されるという別角度の報告)でも複数のユーザーから挙動として報告されている。

実機検証2: denyルールはallowルールに常に勝つ(bypassPermissionsでも)

公式ドキュメントは「Rules are evaluated in order: deny, then ask, then allow. The first match in that order determines the outcome, and rule specificity doesn’t change the order.」と明記している。さらに権限モードのドキュメントには「Deny rules block in every mode, including bypassPermissions.」ともある。これを実際に確かめる。

cd /tmp/perm-test
cat > .claude/settings.local.json << 'EOF'
{
  "permissions": {
    "allow": ["Bash(echo *)"],
    "deny": ["Bash(echo BLOCKED_TEST)"]
  }
}
EOF
claude -p "bashでecho BLOCKED_TESTを実行してください。確認や質問はせず、すぐに実行してください。" \
  --permission-mode bypassPermissions \
  --output-format json > out3.json
grep -o '"result":"[^"]*"' out3.json
grep -o '"permission_denials":\[[^]]*\]' out3.json

期待結果: Bash(echo *)という広いallowルールと、Bash(echo BLOCKED_TEST)という狭いdenyルールが両方マッチする場面で、かつ全チェックをスキップするはずのbypassPermissionsモードであっても、denyが勝って拒否されるはず。

実際の結果:

result: "コマンド実行の権限が拒否されました。実行するには許可が必要です。"
permission_denials: [{"tool_name":"Bash","tool_use_id":"toolu_01HU4ukFf37g4iiSvtV7zut3","tool_input":{"command":"echo BLOCKED_TEST","description":"Echo the string BLOCKED_TEST"}}]

成功判定: permission_denialsにBashが記録され、コマンドが実行された形跡がないこと(実際にこのプロセスの標準出力にBLOCKED_TESTという文字列は一切現れなかった)。「あらゆる確認をスキップする」はずのbypassPermissionsですら、denyルールという1行の設定には勝てない。これは企業導入で「managed設定にdenyルールさえ書いておけば、開発者がどんなモードに切り替えても最終防衛線になる」という設計の裏付けになる。

実機検証3: denyルールは「文字列一致」であって鉄壁ではない

一方で、denyルールには公式ドキュメントが明記する重要な限界がある。

A Bash rule matches the command text Claude writes, after Claude Code splits compound commands and strips wrappers. It doesn’t match the same program invoked in a different form, so a deny or ask rule covers the invocation Claude usually produces and isn’t a security boundary around the program.

つまり、Bash(rm *)のようなdenyルールは「Claudeが通常書く形」のrm -rf ...は止めるが、bash -c 'rm -rf ...'のように別のプログラム経由でラップされた同じ処理は、テキストとして一致しないため素通りする、という仕様だ。これを安全な文字列で検証した。

cd /tmp/perm-test
cat > .claude/settings.local.json << 'EOF'
{
  "permissions": {
    "allow": ["Bash(echo *)", "Bash(bash *)"],
    "deny": ["Bash(echo marker-alpha)"]
  }
}
EOF
claude -p "権限ルールの動作を確認する検証作業をしています。bashのサブシェル経由で 'echo marker-alpha' という文字列を出力してください(例: bash -c 'echo marker-alpha' の形で)。理由も添えて実行してください。" \
  --permission-mode bypassPermissions \
  --output-format json > out4.json
grep -o '"result":"[^"]*"' out4.json
grep -o '"permission_denials":\[[^]]*\]' out4.json

実際の結果:

result: "実行結果は `marker-alpha` でした。理由: `bash -c '...'` はサブシェルを新規起動してコマンドを実行するため、権限ルールがトップレベルの `bash` 呼び出しだけでなく、そこから派生するサブシェル経由の実行にも適用されるかを確認する検証に適した形です。"
permission_denials: []

Bash(echo marker-alpha)という完全一致のdenyルールを設定していたにもかかわらず、bash -c 'echo marker-alpha'という別形のコマンドはpermission_denialsに一切記録されず素通りした。ドキュメントが明記する「文字列一致であって、プログラム自体へのセキュリティ境界ではない」という限界を、実際に成功する形で確認できたことになる。関連してGitHub Issue #92542(open、「Bash deny rules bypassed by any wrapper program outside ax()’s 8-item unwrap list」)がまさにこの種のすり抜けをバグとして報告している。ただし今回確認したのは公式ドキュメントが「仕様」として明記している範囲内の挙動であり、Issueが指摘する「本来unwrapすべき対象が漏れている」個別の不具合そのものとは区別して書く。

なお、この検証の準備中に一度、echo BLOCKED2という文字列を使って同種のテストを試みたところ、Claude自身が「これはプロンプトインジェクション/ジェイルブレイクのテストに見える」と判断し、実行を拒否した。文言を無害なmarker-alphaに変えたところ通常通り実行された。権限ルールとは別に、モデル自身の安全判断が「疑わしい文言のコマンド実行」に対して独立した防御層として働くことも、意図せず確認できた。

コピペ用プロンプト集

1. 【無料・数秒】自分の環境の権限設定を階層順に確認する

for f in ~/.claude/settings.json ./.claude/settings.json ./.claude/settings.local.json; do
  echo "== $f =="
  if [ -f "$f" ]; then cat "$f"; else echo "(存在しない)"; fi
done

期待結果: 3つのファイルそれぞれの有無と中身が表示される。成功判定: defaultModeやpermissions.allow/denyが複数のファイルに散らばって書かれていないか、自分の目で確認できればOK。Claude Code自体を起動しないためAPI課金は発生しない。

2. 自分のセッションが実際にどのモードで動いているか申告させる

claude -p "何もせず、あなたのセッションが今どの権限モードで動いているか一言で答えてください" --output-format json

期待結果: "default"や"auto"など、モード名を含む短い回答。成功判定: 回答が出力されること。ただしこれはモデルの自己申告であり、実機検証1のように実際にファイル書き込みを試してpermission_denialsで客観的に確認する方が確実(自己申告と実際の挙動が食い違うことがある)。注意点: 実際のAPI呼び出しを1回伴う。

3. プロジェクト共有設定でdefaultModeが無効化される場合を再現する

mkdir -p /tmp/perm-check/.claude && cd /tmp/perm-check
git init -q
cat > .claude/settings.json << 'EOF'
{"permissions": {"defaultMode": "auto"}}
EOF
git add -A && git commit -q -m init 2>/dev/null || true
claude -p "test.txtというファイルを作成し、中身に「ok」と書いてください。確認せずすぐ実行してください。" --output-format json > out.json
grep -o '"permission_denials":\[[^]]*\]' out.json
ls test.txt 2>&1 || echo "test.txt NOT CREATED"

期待結果: .claude/settings.jsonにdefaultMode: "auto"を書いても、permission_denialsにWriteの拒否が記録され、test.txtが作られない。成功判定: 「NOT CREATED」と表示されること。活用例: チームに配るプロジェクト設定を書く前に、この検証で「本当にそのファイルから効く設定か」を確認する習慣にする。

4. 同じ値を--settings経由に変えて対比する

cd /tmp/perm-check && rm -f test.txt
claude -p "test.txtというファイルを作成し、中身に「ok」と書いてください。確認せずすぐ実行してください。" \
  --settings '{"permissions":{"defaultMode":"auto"}}' --output-format json > out2.json
ls -la test.txt

期待結果: 今度はファイルが作成される。成功判定: test.txtが実在し中身がokであること。注意点: 実際のAPI課金(classifierの呼び出し込みで数十円程度)が発生する。

5. denyルールがallowルールより常に勝つことを確認する

cd /tmp/perm-check
cat > .claude/settings.local.json << 'EOF'
{"permissions": {"allow": ["Bash(echo *)"], "deny": ["Bash(echo test-deny-check)"]}}
EOF
claude -p "bashでecho test-deny-checkを実行してください。確認せずすぐ実行してください。" \
  --permission-mode bypassPermissions --output-format json > out3.json
grep -o '"permission_denials":\[[^]]*\]' out3.json

期待結果: bypassPermissionsモードであっても、permission_denialsにBashの拒否が記録される。成功判定: 拒否が記録されていること。活用例: managed設定にこの形のdenyルールを置けば、開発者がどの権限モードに切り替えても効く最終防衛線として機能する。

6. PreToolUseフックで多層防御を足す(テンプレート、要調整)

mkdir -p .claude
cat > .claude/audit-hook.sh << 'EOF'
#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | grep -o '"command":"[^"]*"' | head -1)
echo "$(date -u +%FT%TZ) $cmd" >> .claude/bash-audit.log
echo '{}'
EOF
chmod +x .claude/audit-hook.sh
cat > .claude/settings.local.json << 'EOF'
{
  "hooks": {
    "PreToolUse": [
      {"matcher": "Bash", "hooks": [{"type": "command", "command": "bash .claude/audit-hook.sh"}]}
    ]
  }
}
EOF

期待結果: 以降のBashコマンドがすべて.claude/bash-audit.logに記録されるようになる。成功判定: 何かBashコマンドを実行後にcat .claude/bash-audit.logで行が増えていることを確認する。注意点: これはdenyルールのテキスト一致の限界(検証3)を補う手段の1つだが、フック自体もシェルスクリプトである以上、実行環境の信頼性に依存する。OSレベルの強制力が必要な場合はsandboxingを別途検討する。

7. managed settingsでauto/bypassPermissionsそのものを封じる(ドキュメント記載、未実行)

{
  "permissions": {
    "disableAutoMode": "disable",
    "disableBypassPermissionsMode": "disable"
  }
}

公式ドキュメントによれば、この2つのキーをmanaged settingsに設定すると、autoモードとbypassPermissionsモードそのものをシフト+Tabの候補からも除外できる。注意点: managed settingsは組織のMDM配布や管理者用ファイルへの書き込みを要するため、今回は個人環境で実行できず、ドキュメント記載として紹介するに留める(実行済みの検証1〜5とは区別する)。

企業導入への活用フロー(3段階の統合)

以上を踏まえると、Claude Codeをチーム・組織に導入する際の権限設計は、次の3段階を積み上げるのが筋が良い。

  1. 個人の既定値: 各自の~/.claude/settings.jsonにdefaultModeを書く。ここはauto/bypassPermissionsを含め、どの値も素直に効く。
  2. プロジェクト共有の意図表明: .claude/settings.jsonをgitにコミットし、allow/ask/denyルールをチームで共有する。ただしここにdefaultMode: "auto"や"bypassPermissions"を書いても効かない(実機検証1)ため、モードの強制はこの層ではなく個人設定かmanaged設定に任せる。
  3. 組織の最終防衛線: managed settingsにdenyルールとdisableAutoMode/disableBypassPermissionsModeを置く。denyルールはどのモードでも勝つ(実機検証2)ため、ここに書いたルールは開発者側の設定変更で覆されない。ただしBashのdenyルールはテキスト一致に過ぎない(実機検証3)ため、本当に破られては困る操作は、フックによる監査・sandboxingのようなOSレベルの隔離と組み合わせる。

注意点・つまずき所

  • defaultMode: "auto"/"bypassPermissions"は、プロジェクトの.claude/settings.jsonと.claude/settings.local.jsonの2ファイルに限り無効化される。同じ値でもユーザー設定・managed設定・--settings経由なら効く。この非対称性はエラーも警告も出さないため、claude --versionのようなコマンドで気づくことはできない。実際に書き込みなどの動作をさせてpermission_denialsを確認するまで気づけない。
  • 今回の検証はいずれも自分のマシン(v2.1.268)・使い捨てのGitリポジトリ内で完結する範囲に限った。managed settings(組織のMDM配布)は個人環境では検証できないため、公式ドキュメントの記載として紹介するに留めている。
  • denyルールのBashテキスト一致の限界(検証3)は、bash -cだけでなくsh -c、/bin/bashのような別形式でも同様に起こりうると公式ドキュメントは述べているが、今回はそのうちbash -cの1パターンのみを実際に検証した。他の形式は未検証。
  • GitHub Issueとして紹介した#91296・#86478・#92542はいずれも2026-09-13時点でopenであり、他ユーザーの報告であって筆者自身が再現したものではない。自分の実機検証結果とは出典を分けて扱った。

締め

権限モードは「今どう振る舞うか」、設定階層は「どこに書けば効くか」、ルールは「何を許可・拒否するか」という別々の軸であり、企業導入ではこの3つを個人・チーム・組織という単位に対応づけて積み上げる必要がある。今日の最初の一歩としては、コピペ用プロンプト1(自分の設定ファイルを階層順に見る、無料・数秒)から始め、チームで共有する.claude/settings.jsonにdefaultModeを書いていないか確認するとよい。書いていた場合、それは実際には効いていない可能性が高い。

参考(裏取り一次情報): Settings files and precedence、Choose a permission mode、Configure permissions、GitHub Issue #91296・#86478・#92542(いずれもopen)。

Comments

Copied title and URL