パーミッションの仕組みが全くわからなかったので調査しました。
以下調査結果です。
※Claude Code v2.1.44 のバイナリをリバースエンジニアリングして得られた調査結果に基づく。
目次
- パーミッションモード
- 設定ファイルの構造
- ツール別のパーミッション判定
- Bashコマンドの判定フロー(重要)
- ファイル操作ツールの判定フロー
- MCPツールの判定フロー
- Allowパターンの書式とマッチング
- よくある誤解と落とし穴
- 実践的なパターン設計ガイド
1. パーミッションモード
Claude Code には5つのパーミッションモードがある。
| モード | 説明 | Bash | Edit/Write | Read/Glob |
|---|---|---|---|---|
default |
標準動作。危険な操作時に確認を求める | 確認あり | 確認あり | cwd内は自動許可 |
acceptEdits |
ファイル編集を自動承認 | 確認あり | 自動許可 | cwd内は自動許可 |
plan |
分析のみモード。実際のツール実行なし | 実行不可 | 実行不可 | 実行不可 |
bypassPermissions |
すべての権限チェックをバイパス | 自動許可 | 自動許可 | 自動許可 |
dontAsk |
確認プロンプトを出さず、未承認は拒否 | 拒否 | 拒否 | 拒否 |
設定方法
# 起動時オプション
claude --permission-mode acceptEdits
# settings.json で設定
{
"permissions": {
"defaultMode": "acceptEdits"
}
}
注意事項
-
bypassPermissionsはallowDangerouslySkipPermissionsが有効な場合のみ使用可能 - 組織ポリシー(Statsig gate
tengu_disable_bypass_permissions_mode)や設定(disableBypassPermissionsMode: "disable")で無効化可能 -
acceptEditsはファイル編集のみを自動許可する。Bashコマンドには効果がない
2. 設定ファイルの構造
設定ファイルの場所と優先順位
パーミッションルールは複数ソースから読み込まれ、以下の順序で評価される:
| ソース | 説明 |
|---|---|
policySettings |
組織ポリシー(読み取り専用) |
flagSettings |
フィーチャーフラグ(読み取り専用) |
userSettings |
ユーザー設定(~/.claude/settings.json) |
projectSettings |
プロジェクト設定(.claude/settings.json) |
localSettings |
ローカル設定(.claude/settings.local.json) |
cliArg |
CLI引数 |
command |
コマンドライン(読み取り専用) |
session |
セッション中に追加された一時ルール |
settings.json の基本構造
{
"permissions": {
"defaultMode": "default",
"allow": [
"Bash(git *)",
"Read(/home/user/**)",
"Edit(/home/user/**)",
"mcp__serena__*"
],
"deny": [
"Bash(rm -rf /*)"
]
}
}
3. ツール別のパーミッション判定
Claude Code のツールは、パーミッション判定の仕組みがツールの種類によって大きく異なる。
ツール名の内部マッピング
| 表示名 | 内部名 | パーミッション判定方式 |
|---|---|---|
| Bash | Bash |
コマンドパターンマッチング |
| Edit | Edit |
パスベース |
| Write | Write |
パスベース |
| Read | Read |
パスベース |
| Search(UI表示)/ Glob(内部名) | Glob |
パスベース |
| Grep | Grep |
パスベース |
| MCPツール | mcp__{server}__{tool} |
ツール名マッチング |
4. Bashコマンドの判定フロー(重要)
Bashコマンドのパーミッション判定は最も複雑で、多段階の処理を経る。
全体フロー(xgA 関数)
入力コマンド
│
▼
[1] 構文パース(fM関数)── 失敗 → ask
│
▼
[2] サンドボックスチェック(有効時)── サンドボックス内 → allow
│
▼
[3] 完全一致チェック(nS$関数)── deny/ask/allow → 即返却
│ passthrough → 続行
▼
[4] プロンプトルール評価(AI判定)
│
▼
[5] ヒアドキュメント展開(YHB関数)
│
▼
[6] コマンドインジェクションチェック(tb関数)
│
▼
[7] コマンド分割(nJ関数)★超重要★
│ ↓
│ 「&&」「||」「;」「|」で分割
│ リダイレクション(2>/dev/null等)を除去
│ ↓
│ 結果: 個別コマンドの配列
│
▼
[8] cd コマンドのカウント(複数cd → ask)
│
▼
[9] 各サブコマンドを個別に判定(mHB関数) ★超重要★
│
▼
[10] パスセキュリティチェック(KN$関数)
│
▼
[11] 最終判定
コマンド分割の詳細(nJ 関数)
これが最も重要な発見事項。 Bashコマンドは、Allowパターンとのマッチング前に分割・加工される。
分割の例
# 入力コマンド
ls /home 2>/dev/null && ls /tmp 2>/dev/null
# nJ関数による処理後
["ls /home", "ls /tmp"]
# ↑ 「&&」で分割され、「2>/dev/null」は除去される
# 入力コマンド
find /home -name "*.php" 2>/dev/null | head -20
# nJ関数による処理後
["find /home -name \"*.php\"", "head -20"]
# ↑ 「|」で分割され、「2>/dev/null」は除去される
分割ルール
| 演算子 | 動作 |
|---|---|
&& |
コマンド境界として分割 |
|| |
コマンド境界として分割 |
; |
コマンド境界として分割 |
| |
コマンド境界として分割 |
2>/dev/null |
除去される |
2>&1 |
除去される |
> /dev/null |
除去される |
サブコマンドの個別判定(mHB 関数)
分割された各サブコマンドは mHB 関数で個別に判定される。
サブコマンド(例: "ls /home")
│
▼
[1] 完全一致チェック(nS$) ── deny/ask → 即返却
│
▼
[2] プレフィックスマッチング(tgA, "prefix"モード)
│ deny/ask → 即返却
│
▼
[3] ★パスセキュリティチェック(KN$)★ ── ask/deny → 即返却
│ ※ Allowルールより先に評価される!
│
▼
[4] 完全一致のallow判定
│
▼
[5] プレフィックスのallow判定
│
▼
[6] 危険コマンドチェック(lvD)── rm等の危険操作
│
▼
[7] ファイル書き込みチェック(wHB)
│
▼
[8] 読み取り専用コマンド判定 ── 読み取りのみ → allow
│
▼
[9] passthrough(最終的にaskへ)
重大な発見: KN$(パスセキュリティチェック)はAllowルールより先に評価される。
つまり、cwd外のパスにアクセスするコマンドは、Allowパターンに一致していてもブロックされる可能性がある。
パターンマッチング関数(rgA 関数)
// パターンマッチング前の前処理
let commandWithoutRedirections = vZ(command).commandWithoutRedirections;
// ↑ リダイレクションを除去
let normalizedCommand = kyA(commandWithoutRedirections);
// ↑ timeout, time, nice, nohup, 環境変数プレフィックスを除去
パターンの3つのタイプ(ogA 関数でパース):
| タイプ | パターン例 | マッチング方式 |
|---|---|---|
exact |
Bash(ls) |
完全一致のみ |
prefix |
Bash(git:*) ※旧形式 |
前方一致(startsWith) |
wildcard |
Bash(git *) |
グロブマッチング(agA関数) |
ワイルドカード * は任意の文字列にマッチする(スペース含む)が、マッチング対象は分割後・リダイレクション除去後のコマンドである点に注意。
5. ファイル操作ツールの判定フロー
Read, Edit, Write, Glob(Search), Grep はすべてパスベースの判定を使用する。
判定フロー(Eo 関数 - 読み取り系、t1H 関数 - 編集系)
ファイルパス
│
▼
[1] UNCパスチェック(\\\\、//で始まるパス → ask)
│
▼
[2] Windowsパス不正パターンチェック
│
▼
[3] deny ルール評価(eQ関数)
│
▼
[4] ask ルール評価(eQ関数)
│
▼
[5] モードチェック(acceptEditsモード等)
│
▼
[6] ワーキングディレクトリチェック(ax関数)
│ cwd内 → allow(defaultモード)
│
▼
[7] allow ルール評価(eQ関数)
│
▼
[8] ask(デフォルト)
パスルールのマッチング(eQ 関数)
eQ 関数は ignore npmパッケージ(gitignoreと同じ形式)を使用してパスマッチングを行う。
// 相対パスを計算
let relativePath = path.relative(baseDir, targetPath);
// 「../」で始まる場合はスキップ(マッチしない)
if (relativePath.startsWith(`..${separator}`)) continue;
// ignoreパッケージでマッチング
let result = ignore().add(patterns).test(relativePath);
重要: パスがベースディレクトリの外にある場合(相対パスが ../ で始まる場合)、ルールはスキップされる。
パスパターンの書式
{
"allow": [
"Read(/home/user/**)", // /home/user/ 以下すべて
"Read(~/**)", // ホームディレクトリ以下すべて
"Edit(/tmp/**)", // /tmp/ 以下すべて
"Write(./.claude/**)" // プロジェクト内 .claude/ 以下
]
}
Glob/Search ツールの特殊性
UI上は「Search」と表示されるが、内部名は「Glob」。パーミッション判定は Read と同じ Eo 関数を使用する。
Glob/Search ツール
│
▼
getPath() でパスを取得
│
▼
Eo() で Read と同じパス判定
そのため、Search(**) や Glob(**) のようなパターンはパラメータベースではなくパスベースで判定される。
ただし Glob(**) のようなワイルドカード記法がうまく機能するかはパスの解決方法に依存する。
6. MCPツールの判定フロー
MCP(Model Context Protocol)ツールは独自のシンプルな判定フローを使用する(yHB 関数)。
MCPツール呼び出し
│
▼
[1] サーバー名・ツール名の文字種チェック(英数字、ハイフン、アンダースコアのみ)
│
▼
[2] 内部名を構築: mcp__{server}__{tool}
│
▼
[3] deny ルール評価 → deny
│
▼
[4] ask ルール評価 → ask
│
▼
[5] allow ルール評価 → allow
│
▼
[6] デフォルト → ask
MCPツールのパターン例
{
"allow": [
"mcp__serena__*", // serenaサーバーの全ツール
"mcp__playwright__*", // playwrightサーバーの全ツール
"mcp__gdrive__read_file" // gdriveサーバーのread_fileのみ
]
}
7. Allowパターンの書式とマッチング
基本書式
ToolName(specifier)
| 要素 | 説明 | 例 |
|---|---|---|
ToolName |
ツール名 |
Bash, Read, Edit, Write, Glob, Grep
|
specifier |
マッチング対象 | コマンド文字列 or パス |
パターンタイプ
| タイプ | 書式 | 例 | マッチ対象 |
|---|---|---|---|
| 完全一致 | Bash(ls) |
ls のみ |
完全に同一のコマンド |
| プレフィックス(旧形式) | Bash(git:*) |
gitで始まるコマンド |
: の後が *
|
| ワイルドカード | Bash(git *) |
git status, git diff 等 |
* は任意の文字列 |
ワイルドカード * の動作
* はグロブスタイルで任意の文字列(スペース含む)にマッチする。
パターン: Bash(git *)
マッチ: git status ✅
マッチ: git diff --cached ✅
不一致: ls ❌
ただし、Bashの場合、マッチング対象は分割後・リダイレクション除去後のサブコマンド。
8. よくある誤解と落とし穴
誤解1: 複合コマンドはそのままマッチングされる
実際の動作: &&, ||, ;, | で分割され、各部分が個別にマッチングされる。
❌ 誤った想定:
パターン: Bash(ls * && ls *)
コマンド: ls /home && ls /tmp
→ パターン全体でマッチングされる
✅ 実際の動作:
コマンド: ls /home && ls /tmp
→ 分割: ["ls /home", "ls /tmp"]
→ "ls /home" を Bash(ls *) でマッチ → ✅
→ "ls /tmp" を Bash(ls *) でマッチ → ✅
→ 結果: 両方マッチすれば allow
※ Bash(ls * && ls *) というパターンは永遠にマッチしない
誤解2: リダイレクション付きパターンが必要
実際の動作: リダイレクション(2>/dev/null, 2>&1, > file 等)はマッチング前に除去される。
❌ 不要なパターン:
Bash(find * 2>/dev/null)
Bash(ls * 2>/dev/null && ls * 2>/dev/null)
✅ 必要なパターン:
Bash(find *) ← これだけで十分
Bash(ls *) ← これだけで十分
※ リダイレクション付きパターンは害はないが、マッチすることもない
誤解3: for/if等の複合構文のパターン
実際の動作: for、if、while 等のシェル構文はシェルパーサー(qYH 関数)によって1つの構文単位として認識され、内部の ; ではトップレベルの分割は行われない。ただし、以下の不確実性がある。
不確実な点: glob の * が ; にマッチするかは実装依存。
パターン: Bash(for * in *; do *; done)
コマンド: for f in *.txt; do echo $f; cat $f; done
もし * が ; にマッチするなら:
→ * が "echo $f; cat $f" にマッチ → ✅(このパターンだけで十分)
もし * が ; にマッチしないなら:
→ マッチしない → ❌
→ Bash(for * in *; do *; *; done) が必要
安全策: * が ; にマッチしない場合に備え、本体の ; の数が異なるバリエーションパターンを追加しておく。
"Bash(for * in *; do *; done)",
"Bash(for * in *; do *; *; done)",
"Bash(for * in *; do *; *; *; done)",
"Bash(for * in *; do *; *; *; *; done)"
なお、構文の外側(done の後)に && がある場合は通常のトップレベル分割が適用されるため、Bash(for * in *; do *; done && *) は無効。
誤解4: Glob/Search はパラメータベース
実際の動作: Glob/Search のパーミッションは Read と同じパスベースの判定を使用する。
❌ 効果が不明確:
Search(**)
Glob(*, *)
✅ 実際に効くのはパスベースルール:
Read(/home/user/**) ← これがGlob/Searchにも適用される
誤解5: Allow に書けばすべて許可される
実際の動作: Bash の mHB 関数では、KN$(パスセキュリティチェック)が Allow ルールより先に評価される。
判定順序:
1. deny ルール
2. ask ルール
3. ★ KN$(パスセキュリティチェック)★ ← ここでブロックされる可能性
4. allow ルール(完全一致)
5. allow ルール(プレフィックス/ワイルドカード)
つまり:
Bash(find *) を Allow に設定しても、
find /etc -name "*.conf" のようにcwd外のパスにアクセスすると
KN$ でブロックされる可能性がある
9. 実践的なパターン設計ガイド
基本原則
- Bashパターンはシンプルなコマンド単位で書く(複合コマンドのパターンは不要)
- リダイレクションは書かない(自動除去される)
- パスアクセスにはRead/Editのパスルールを使う
- MCPツールはサーバー単位のワイルドカードが便利
推奨パターン集
{
"permissions": {
"allow": [
// === Bash: シンプルなコマンド単位 ===
"Bash(git *)",
"Bash(ls *)",
"Bash(cat *)",
"Bash(find *)",
"Bash(grep *)",
"Bash(curl *)",
"Bash(echo *)",
"Bash(mkdir *)",
"Bash(cp *)",
"Bash(mv *)",
"Bash(head *)",
"Bash(tail *)",
"Bash(wc *)",
"Bash(jq *)",
"Bash(sed *)",
// === ファイル操作: パスベース ===
"Read(/home/user/**)",
"Read(/tmp/**)",
"Edit(/home/user/projects/**)",
"Write(/tmp/**)",
// === MCPツール: サーバー単位 ===
"mcp__serena__*",
"mcp__playwright__*"
],
"deny": [
"Bash(rm -rf /*)",
"Bash(rm -rf ~/*)"
]
}
}
不要なパターンの例(追加しても意味がない)
{
"allow": [
// ❌ 複合コマンドパターン(分割されるため無意味)
"Bash(ls * && ls *)",
"Bash(find * | head *)",
"Bash(cat * > /tmp/* && echo *)",
// ❌ リダイレクション付き(除去されるため無意味)
"Bash(find * 2>/dev/null)",
"Bash(ls * 2>/dev/null && ls * 2>/dev/null)",
"Bash(cat * 2>&1 | grep *)",
// ❌ for/if の複雑なパターン(内部の&&で分割される可能性)
"Bash(for * in *; do * && *; done)",
"Bash(if *; then * && *; else *; fi)"
]
}
パスセキュリティチェック(KN$)の回避
cwd 外のパスへのアクセスが KN$ でブロックされる場合:
-
additionalDirectoriesを設定に追加する{ "permissions": { "additionalDirectories": ["/home/user/other-project"] } } -
起動時の
--add-dirオプションを使用するclaude --add-dir /home/user/other-project
付録: ソースコード上の主要関数マッピング
| 関数名 | 役割 |
|---|---|
xgA |
Bash全体のパーミッション判定(エントリポイント) |
nJ |
コマンド分割(&&, |, ; で分割)+ リダイレクション除去 |
mHB |
サブコマンド個別判定 |
nS$ |
完全一致ルールチェック |
rgA |
パターンマッチング(exact/prefix/wildcard) |
tgA |
deny/ask/allow ルール一括評価 |
ogA |
パターン文字列のパース(exact/prefix/wildcard判定) |
agA / OHB
|
ワイルドカード(glob)マッチング |
kyA |
コマンド正規化(timeout, nice, nohup, 環境変数除去) |
vZ |
リダイレクション除去 |
KN$ |
パスセキュリティチェック(cwd外アクセスのブロック) |
tb |
コマンドインジェクションチェック |
Eo |
Read/Glob パーミッション判定 |
t1H |
Edit/Write パーミッション判定 |
eQ |
パスルールマッチング(ignore npmパッケージ使用) |
yHB |
MCPツールパーミッション判定 |
bS1 |
全ツール共通のトップレベル判定 |
cF |
パーミッションモード考慮付き最終判定 |
調査環境
- Claude Code バージョン: 2.1.44
- バイナリ形式: ELF 64-bit (Linux x86-64)
- 調査手法:
stringsコマンドによる文字列抽出 + 関数ロジックの手動デコンパイル - 調査日: 2026-02-17
注意: この文書はリバースエンジニアリングに基づくため、バージョンアップにより動作が変更される可能性がある。