Claude Codeの拒否ルールをeval等ですり抜けられる問題と対策
permissions.denyのRead/Editルールはevalやenv -Cで包んだBashコマンドを検知できません。v2.1.268の修正とv2.1.273のリバートの経緯と、サンドボックスやフックを重ねる対策を解説します。
Claude Codeのpermissions.denyはコマンド文字列だけを見て判定する
Claude Codeのpermissions.denyルールは、Claudeが書いたコマンドの文字列を解析して一致を判定します。Bashルールは、Claudeが普段出力する呼び出しを止める仕組みであり、プログラムそのものを囲うセキュリティ境界ではありません。
この設計のため、eval '...'やenv -C . sh -c '...'のようにシェルへ文字列をそのまま渡す形で包むと、パーサーが中身を解析できなくなります。changelogには、env -C・evalなど権限チェッカーが解析できないコマンドが同じ行にあると、ReadまたはEditの拒否ルールが適用されないことがある、という不具合が記録されています。
Read(./secrets/**)を設定したときに何が止まり、何が止まらないかを分けると次のようになります。
Read(./secrets/**)を設定したときの行き先
拒否ルールが効く形
- Readツールによる読み取り(GrepやGlobにもベストエフォートで適用)
cat・head・tail・sed・teeなど、Claude Codeがファイルを扱うと認識するBashコマンド< fileや> fileのようなリダイレクトの対象
効かない、またはプロンプトに戻る形
eval '...'やenv -Cで包んだ行(v2.1.273以降は確認プロンプト)- ファイル名を書かずに読むコマンド。そのファイルがあるディレクトリでの
grep -r pattern .など - ファイルを自分で開くPythonやNodeのスクリプトのような子プロセス
拒否の範囲を広げては戻された5つのバージョン
Bashの行にどこまで拒否ルールを適用するかは、v2.1.257からの変更履歴で広げては戻すことが繰り返されています。evalの話は、この繰り返しの2回目にあたります。
Read/Edit拒否ルールの適用範囲をめぐる変更
- v2.1.257リダイレクトと読み取りコマンドに適用
Bashの
Read()・Edit()拒否ルールが、< fileのリダイレクトやtac・egrepなどの読み取りコマンドにも効くようになりました。 - v2.1.259 → v2.1.260引数への適用が1バージョンで戻る
v2.1.259でオプションの値(
--ignore-revs-file=.env)やgit diffのオペランドにも広がりましたが、v2.1.260でリバートされました。Read(./**/build/**)のもとでnpm run buildが全モードで拒否され、cd … && grepがautoモードでもプロンプトを出したためです。 - v2.1.268解析できない行にも適用
env -C・evalなど権限チェッカーが解析できないコマンドが同じ行にあっても、ReadとEditの拒否ルールを適用する修正が入りました。 - v2.1.273v2.1.268の修正をリバート
time -p make buildのような正当なコマンドまで拒否されたため、元の「プロンプトで確認する」挙動に戻りました。
文字列の照合を広げるたびに、無関係なコマンドの誤拒否が出て巻き戻されています。照合を狭めれば迂回が残り、広げれば誤拒否が出る。この綱引きが続いているため、拒否ルールの改善だけで迂回を消し切る方向は取りにくい構造と読めます。v2.1.274からv2.1.289までのchangelogには、解析できない行へ拒否ルールを再適用する変更は載っていません。
モード別に見る、evalで包んだ行の行き先
Manualモード(設定値default)では、v2.1.273の挙動どおり、evalやenv -Cで包んだ行にも実行前の確認プロンプトが出ます。ユーザーがそこで気づいて拒否すれば実害はありません。
autoモードでは、行が次の順に判定されます。
autoモードの判定順序(先に一致した段で決着)
- 1
allow・ask・denyルールに一致するか
一致すればその場で決着します。一致しなければ、次の段へ進みます。
- 2
読み取り専用か、作業ディレクトリ内の編集か
当てはまれば自動承認されます。
- 3
それ以外は分類器へ
分類器がブロックすると、Claudeに理由が返ります。
evalの行が拒否ルールに一致しなくても、そこで通ったことにはならず、autoモードなら次の段の判定へ進みます。autoモード側に独自ルールを足す方法はClaude CodeのautoModeに独自ルールを追加する書き方にあります。
一方、分類器はautoモードの仕組みで、bypassPermissionsモード(--dangerously-skip-permissions)にはありません。拒否ルールはbypassPermissionsでも効きますが、解析できない行についてv2.1.273が述べる確認プロンプトは、プロンプトを省くモードでは前提にできません。評価の網が拒否ルールの文字列照合だけになるため、次のような行は実行前に気づく機会がなくなる可能性があります。
# permissions.deny に "Read(./secrets/**)" を設定していても
eval 'cat ./secrets/api-key.txt'
env -C . sh -c 'cat ./secrets/api-key.txt'同じ内容を直接cat ./secrets/api-key.txtと書けば拒否ルールに一致して止まりますが、evalやenv -Cで一段包むだけでパーサーの解析対象から外れます。
同じ設計限界は他のBashルールにも及ぶ
この問題はevalやenv -Cだけの欠陥というより、「コマンド文字列を静的解析して一致を取る」という拒否ルール全体の設計に由来します。denyやaskに置いたBashルールが止める形と止めない形は、次のとおりです。
| ルール | 止まる | 止まらない |
|---|---|---|
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 |
timeout・time・nice・nohup・stdbufのほか、command・builtin・zshのnoglob、フラグなしのxargsは、ルール照合の前に取り除かれる組み込みのラッパーです。Bash(npm test *)がtimeout 30 npm testにも一致するのはこのためです。sh -c・bash -c・evalは取り除かれる一覧に入っていないので、中身を渡すだけのラッパーとして素通りします。止まらない形が実際に実行されるかどうかは、権限モードと他のルールが決めます。コマンド文字列に依存しない境界が必要なら、次節のサンドボックスがその役割を担います。
設定例 — Read/Editの拒否ルール、サンドボックス、PreToolUseフック
保護したいファイルがある場合、settings.jsonにはRead/Editの拒否ルールをパスで指定します。
{
"permissions": {
"deny": ["Read(./secrets/**)", "Edit(./secrets/**)", "Read(./.env)"]
}
}このルールだけでは、前述のeval 'cat ./secrets/api-key.txt'のような行が確認プロンプト頼みになります。そこでサンドボックスを加えますが、sandbox.enabled: trueの既定設定は書き込みを作業ディレクトリ配下に絞るだけで、読み取りは一部の拒否ディレクトリを除きコンピューター全体に開いています。./secrets/は作業ディレクトリの内側にあるので、既定のサンドボックスだけではこの読み取りを止められません。読み取りも塞ぐにはsandbox.filesystem.denyReadにパスを追加します。
{
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["./secrets"]
}
}
}サンドボックスの境界はコマンド文字列ではなく、実行中のプロセスにOSが強制する仕組みです。evalで中身を隠されても、denyReadで指定したパスへのアクセスはブロックされます。ただしdenyReadが止めるのはサンドボックス内のBashコマンドで、Readツールは止まりません。Readツールは上のRead(./secrets/**)が受け持つので、2つの設定は両方そろえて意味を持ちます。
設定の詳細とallowReadでの例外の開け方はsandbox.filesystem.denyReadで認証情報ファイルを読ませないにまとめています。
PreToolUseフックを併用すると、権限ルール評価とは別のレイヤーでコマンド文字列そのものを検査できます。フックはexit code 2で終了するとツール呼び出しを止め、標準エラー出力に書いた理由がClaudeに返ります。まずsettings.jsonでBashツールに登録します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/deny-eval-secrets.sh"
}
]
}
]
}
}検査するスクリプトは次のとおりです。
#!/bin/bash
# .claude/hooks/deny-eval-secrets.sh (PreToolUse, matcher: Bash)
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq '(eval|env -C).*secrets'; then
echo "secretsディレクトリへのeval/env -C経由のアクセスを検知したため拒否します" >&2
exit 2
fiフックはClaude Codeと独立したシェルスクリプトなので、Claude Codeが渡すJSONの形を手で作れば単体で試せます。5種類の入力を流した結果は次のとおりです。
jq -n --arg c "eval 'cat ./secrets/api-key.txt'" '{tool_input:{command:$c}}' | ./deny-eval-secrets.sh; echo "exit=$?"
# secretsディレクトリへのeval/env -C経由のアクセスを検知したため拒否します
# exit=2
# 入力ごとの結果
# env -C . sh -c 'cat ./secrets/api-key.txt' → 同じメッセージで exit=2
# sh -c 'cat ./secrets/api-key.txt' → exit=0(素通り)
# eval 'cat ./sec''rets/api-key.txt' → exit=0(素通り)
# cat ./secrets/api-key.txt → exit=0(フックは何もせず、拒否ルール側で止まる)evalとenv -Cは止まりますが、sh -cで包んだ行や、'sec''rets'のように文字列を分割した行は通ります。簡易フックは、狙った迂回の形だけを止める道具です。キーワードを増やしても書き方の自由度には追いつけないので、境界としてはサンドボックスのdenyReadが土台になります。
層ごとの役割 — permissions.denyだけに頼らない
permissions.denyは、Claudeが書いたコマンドをどう扱うかを決める一次の防波堤です。任意のシェル構文を完全に解析するようには作られていません。守る対象ごとに受け持つ層が違うので、次のように重ねます。
- Readツールの経路は
Read拒否ルール、Bashの子プロセスはsandbox.filesystem.denyRead: 前節のとおり、片方だけでは反対側の経路が開きます。 permissions.blockReadsOutsideWorkingDirectoriesは作業ディレクトリの外を塞ぐ設定: ファイルツールによる作業ディレクトリ外の読み取りを、bypassPermissionsを含む全モードで拒否します。サンドボックスを有効にしていれば、サンドボックス内のコマンドもホームディレクトリなどの読み取りを失います。シェルコマンド自体を拒否するわけではありません。この設定を有効にしている場合、サブシェルや複数回のcdを含む行のようにパーサーが追えないコマンドは、autoモードとbypassPermissionsでも確認プロンプトになります。サンドボックスが読み取りの制限を強制しているコマンドでは、このプロンプトは出ません。./secrets/は作業ディレクトリの内側にあるので、この設定の対象には入りません。- PreToolUseフックでコマンド文字列を自前で検査する: フックはルール評価と独立に動き、
exit code 2の終了は許可ルールがあっても呼び出しを止めます。上の簡易フックのように、通したくない形を具体的に列挙します。 --dangerously-skip-permissionsは隔離環境に限る: このモードはコンテナやVMなど、ホストに被害を出せない環境向けです。ホスト上で使うと確認プロンプトという網が消え、evalによる素通りを止めるのは、サンドボックスとフックのように権限ルールの外側にある仕組みだけになります。
組織で拒否ルールを配布するなら、ルール側を固める方法があります。/permissionsコマンドで権限ルールとauto mode拒否を管理するやallowManagedPermissionRulesOnlyで権限ルールを管理設定だけに縛るが入口です。ただしサンドボックス側の設定も同じ場所に置かないと、この記事の迂回は残ります。
まとめ
permissions.denyのRead/Editルールは、evalやenv -Cのように中身をシェルへ渡す行までは追えません。v2.1.268の修正はv2.1.273でリバートされ、解析できない行は確認プロンプトに戻っています。守りたいパスがあるなら、Readツールは拒否ルール、Bashの子プロセスはdenyReadと、経路ごとに受け持つ層を分け、必要に応じてフックを足します。