Claude CodeのBash権限ルールは複合コマンドをどう評価するか
&&やパイプで繋いだBashコマンドは、Claude Codeの権限ルールがサブコマンドごとに個別評価します。許可したのに確認が出る、denyしたのに通るといった症状の原因をまとめます。
Claude Codeの権限ルールは、&&・||・;・|・|&・&・改行で繋がれた複合コマンドを、1本の文字列ではなくサブコマンドごとに分解して評価します。Bash(safe-cmd *)というallowルールがあっても、safe-cmd && other-cmdはother-cmd側の評価が別に必要です。逆にdenyルールは、どれか1つのサブコマンドに一致すれば複合コマンド全体を止めます。
この記事は「許可したはずなのに確認が出る」「denyに入れたのに通ってしまう」といった症状から、原因をたどる順に並べています。
症状別: どこで食い違いが起きているか
先に判定の流れを置きます。症状の多くは、この4段階のどこかで起きています。
複合コマンドが評価される流れ
- 1
区切り演算子で分割する
7種類の区切りでサブコマンドに分け、各サブコマンドが独立にルールと照合されます。
- 2
ラッパーと環境変数を取り除く
固定リストのラッパーと、既知の安全な環境変数の代入が剥がされます。
- 3
ルールと照合する
各サブコマンドを、allow・deny・askのルールとそれぞれ照合します。効き方の向きは次の節で比べます。
- 4
リダイレクト先を別に検査する
>や<の先のファイルは、コマンドとは別にEditやReadのルールで見られます。
git status && npm testを実行させたいなら、Bash(git status *)とBash(npm test *)の両方が必要です。片方しかなければ、もう一方のサブコマンドで確認プロンプトが立ちます。Claude Codeの権限は、1本のBashルールが複合コマンド全体を覆う作りになっていません。
認識される区切りは改行を含みますが、これは区切り演算子としての改行です。heredocの本体に埋め込まれた改行は別の話で、経緯はClaude Codeで複数行コマンドの自動承認が効かない問題の原因と対処法にまとめています。
allowとdenyで「複合」の効き方が逆になる
同じ分解でも、allowとdeny・askでは向きが逆です。
複合コマンドでのルールの効き方
allowルール
すべてのサブコマンドがルールに一致して初めて自動承認されます。npm test &&のように&&や||の後ろが空のコマンドは解析不能として扱われ、Bash(npm *)があっても承認されません。
deny・askルール
サブシェル・コマンド置換・forループの本体に入れ子になったコマンドにも適用されます。Bash(git clean *)のaskルールは、cd /tmp && git clean -fでもecho "$(git clean -f)"でも確認を求めます。
Bash(rm *)をdenyに置くと、ls && rm -rf tmpはlsの部分が許可されていてもコマンドごと拒否されます。lsだけが先に走ることはありません。
Bash向けのallow・denyは起動時のフラグでも渡せます。実機のclaude --help(v2.1.287で確認)には、次のように出力されます。
claude --help | grep -A2 -E "allowedTools|disallowedTools" --allowedTools, --allowed-tools <tools...>
Comma or space-separated list of tool names to allow (e.g. "Bash(git *)
Edit")
--
--disallowedTools, --disallowed-tools <tools...>
Comma or space-separated list of tool names to deny (e.g. "Bash(git *)
Edit")書式は設定ファイルと同じなので、CIの非対話実行にもそのまま持ち込めます。手順は--allowedTools/--disallowedToolsで権限を絞るにあります。
「Yes, and don't ask again」は複合コマンドをどう保存するか
確認プロンプトで複合コマンドを「Yes, and don't ask again」で承認しても、複合コマンド全体が1本のルールになることはありません。保存されるのは、承認が必要だったサブコマンドごとの個別ルールです。
git status && npm testを承認すると、保存されるのはnpm testのルールだけです。git statusは元々読み取り専用で確認が要らないからです。以降はnpm testが単独でも、他のコマンドの後ろでも、そのルールで認識されます。
作業ディレクトリの外へcdするサブコマンドは、そのパス用のReadルールが別に保存されます。1回の複合コマンド承認で保存されるルールは最大5件です。python manage.py makemigrations && python manage.py migrateのような開発コマンドも同じ扱いで、実例はClaude CodeでDjangoアプリをMVT構成で開発する手順で扱っています。
ラッパーが剥がされるものと、剥がされないもの
ルールとの照合の前に、Claude Codeは固定リストのラッパーを取り除いて中身のコマンドを見ます。Bash(npm test *)はtimeout 30 npm testにも一致します。環境変数の扱いにも差があります。
NODE_ENV=test npm testのように既知の安全な変数が先頭にあるときは、これも除去されてBash(npm test *)に一致します。未知の変数の代入をまたぐと、allowルールは一致しません。一方でdenyとaskは、どんな代入も素通りして一致します。Bash(rm *)をdenyに置けば、FOO=bar rm -rf tmp/も止まります。
フラグなしのxargsも除去対象です。Bash(grep *)はxargs grep patternに一致しますが、xargs -n1 grep patternのようにフラグが付くとxargsそのものとして扱われます。内側のコマンド用のルールでは対応できません。
| ラッパー | 除去されるか | 備考 |
|---|---|---|
timeout / time / nice / nohup / stdbuf | 除去されるかされる | 備考引数を実際のコマンドとして実行するため |
command / builtin / zshのnoglob | 除去されるかされる | 備考同上 |
| 既知の安全な環境変数の先頭代入 | 除去されるかされる | 備考未知の変数はallowルールのみ素通りしない |
フラグなしのxargs | 除去されるかされる | 備考フラグ付きは対象外 |
command -v(問い合わせ形) | 除去されるかされない | 備考中身を実行せず所在を調べるだけの形 |
zshのnocorrect | 除去されるかされない | 備考除去リストの対象外 |
watch / setsid / ionice / flock | 除去されるかされない | 備考プレフィックスルールでは自動承認できず、Manual modeでは常に確認が出る |
devbox run / npx / direnv exec / mise exec / docker exec | 除去されるかされない | 備考開発環境ランナーはリストに含まれない |
このリストは組み込みで、設定から追加や変更はできません。
環境ランナーのルールは広く通りすぎる
devbox run・direnv exec・mise exec・npx・docker execは、引数をそのままコマンドとして実行します。ところが除去リストにないため、Bash(devbox run *)というルールはrunの後ろに続く任意のコマンドに一致します。devbox run rm -rf .も例外ではありません。
ランナー越しに特定のコマンドだけを許可したいなら、ランナーと中身をセットにしたBash(devbox run npm test)のような具体的なルールを、許可したい中身の数だけ書きます。
findに-execや-deleteが付く形も、Bash(find *)では扱えません。承認したいときは、そのコマンド全文に対する完全一致のルールを書きます。
リダイレクト先は別のチェックを受ける
>・>>・2>は複合コマンドの区切りではなく、リダイレクト先ファイルへの書き込みとして別に検査されます。この検査にはEditのallow・denyルール、保護パス、作業ディレクトリが使われます。Bash(git commit *)はコマンドを許可するだけで、書き込み先までは許可しません。
~で始まるパスや、ワイルドカードを含むパスへのリダイレクトは承認が必要です。
入力側の< fileは、Readのルールと作業ディレクトリで検査されます。作業ディレクトリの外はReadのallowルールが無い限り承認が必要です。同じコマンド内でcdした後の相対パスやワイルドカードを含むパスは、allowルールがあっても承認が必要になります。この入力リダイレクトの検査はv2.1.257以降です。
検査されないのは、/dev/null、2>&1のようなファイル記述子の形、そしてheredocとhere-stringです。ファイルの実体がないためです。
読み取り専用コマンド同士の複合は通るが例外がある
ls・cat・grepのような組み込みの読み取り専用コマンドは、各サブコマンドが単独で読み取り専用と判定できる限り、繋いでも確認なしで動きます。cd packages/api && lsが好例です。
例外は、個々には読み取り専用でも確認が出る形です。
cdとgit:cdが別のディレクトリへ移るとき。移動先のgit hookが走る可能性があるためですcdとリダイレクト: 移動後にリダイレクト先がどこに解決されるか判断できないとき(/dev/null宛てだけなら出ません)- 解析できないコマンド: 解析しきれない場合は、読み取り専用とはみなされず確認が出ます。10,000文字を超えるコマンドも常に確認が出ます
自動承認される条件の全体像はClaude Codeで読み取り専用コマンドが自動承認される条件に、cd側に確認が出る理由はClaude Codeでcdに許可プロンプトが出る原因と対処法にあります。
ワイルドカードの空白と、denyが止めない別の書き方
ルールの書き方でも取りこぼしが出ます。Bash(ls *)はls -laと、引数なしのlsに一致しますが、lsofには一致しません。空白が末尾の*の一部だからです。Bash(ls*)と書くとlsofにも一致します。
もう1つ注意したいのが、*が置かれた位置の中身を限定しない点です。Bash(git * main)の*は、git -c core.fsmonitor=<script> diff mainのように-cで外部プログラムを走らせる形にも一致します。起動時に出る警告の読み方と書き換えはBash許可ルールのワイルドカード警告にあります。
denyやaskは、Claudeが普通に書くコマンドの形を止めるもので、プログラムそのものの境界ではありません。止められる形と、止められない形は次のとおりです。
| ルール | 止まる例 | 止まらない例 |
|---|---|---|
Bash(curl *) | 止まる例curl https://example.com | 止まらない例/usr/bin/curl https://example.com / sh -c 'curl https://example.com' |
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 / git 'push' origin main |
止まらなかった形が通るか確認が出るかは、ほかのルールと権限モードが決めます。コマンドの文字列に依存しない制限が必要なときは、サンドボックスを使います。
引数を絞るパターンはなぜ壊れやすいか
Bash(curl http://github.com/ *)のような「特定URL宛てのcurlだけ許可する」ルールは、次のような形で簡単に外れます。
- オプションの前置:
curl -X GET http://github.com/はパターンの先頭一致から外れます - プロトコルの差:
https://github.com/はhttpと一致しません - リダイレクト:
curl -L http://short.example.com/xyzのような短縮URLは、最終的な接続先がパターンの外でも止められません - 変数展開:
URL=http://github.com && curl $URLは、変数に入れた時点で文字列一致が成立しません
URLを確実に絞りたいときの代替案は3つあります。
curl・wgetをdenyに置き、WebFetch(domain:github.com)を使う: ネットワークアクセスをBashの外に出します。ただしdenyはsh -c経由や絶対パスの呼び出しを止めないため、厳密に守るならサンドボックスのネットワーク許可リストと組み合わせます- PreToolUseフックでURLを検証する: コマンド全文を自前のロジックで調べられます
- CLAUDE.mdに書く: Claudeの試行を方向づけるだけで境界にはなりません。上の2つと併用します
WebFetchだけではネットワークアクセスは防げません。Bashが許可されていれば、Claudeはcurlなどで任意のURLに届きます。
よくある質問
PowerShellでも同じように複合コマンドを分解しますか
分解します。Claude CodeはPowerShellのASTを解析し、複合コマンド内の各コマンドを個別にチェックします。|と;、PowerShell 7以降では&&と||が分割の対象で、複合コマンドが許可されるには全サブコマンドがルールに一致する必要があります。
PreToolUseフックで複合コマンドの穴を塞げますか
塞げます。フックはコマンド全文を見て判定でき、終了コード2で返せばallowルールがあってもツール呼び出しは止まります。Bashをallowリストに丸ごと入れて確認プロンプト自体を止め、ブロックしたい特定のコマンドだけをフックで拒否する構成もできます。
逆向きの注意点もあります。フックが"allow"を返しても、一致するdenyルールはブロックし、一致するaskルールは確認を求めます。
保存されたルールは/permissionsから確認できますか
承認で保存されたルールはリポジトリ直下の.claude/settings.local.jsonに書かれ、/permissionsはすべてのルールを出どころのファイルと一緒に一覧します。操作方法は/permissionsコマンドで権限ルールとauto mode拒否を管理するで扱っています。括弧が崩れたルールは読み込まれずにスキップされるため、Invalid permission ruleの直し方も合わせて確認できます。
まとめ
確認が出たときも、止めたはずのコマンドが通ったときも、最初に見るのは「どのサブコマンドで起きたか」です。次に、その手前でラッパーや環境変数が剥がされているか、ランナーに包まれていないかを確かめます。原因はたいていこの2点のどちらかに収まります。繰り返し承認している組み合わせは、/fewer-permission-promptsで許可プロンプトを自動整理するでallowリストに反映できます。