Claude Media
Claude Codeの拒否ルールをeval等ですり抜けられる問題と対策

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が実行しようとするコマンドの文字列を解析して一致判定します。一致の対象はClaudeが書いたコマンドのテキストそのものであり、プログラムそのものを囲うセキュリティ境界ではありません。ファイルシステムやプロセスの実態を見ているわけではありません。

この設計のため、eval '...'やenv -C . sh -c '...'のようにシェルへ文字列をそのまま渡す形でBashコマンドを包むと、パーサーが中身を解析できなくなります。公式changelogには、env -C、eval、あるいは同種の権限チェッカーが解析できないコマンドが同じ行にあると、ReadまたはEditの拒否ルールが適用されないことがある、という不具合が記録されています。

v2.1.268で一度直り、v2.1.273でリバートされた経緯

このバイパスは一度は修正が入りましたが、その修正自体が別の副作用を起こして巻き戻されています。

バージョン変更内容
v2.1.268変更内容env -C・evalなど解析不能なコマンドが同じ行にあるとRead/Editの拒否ルールが効かない不具合を修正
v2.1.273変更内容v2.1.268の修正をリバート。time -p make buildのような正当なコマンドまで拒否ルールに引っかかって拒否されるようになったため、元の「プロンプトで確認する」挙動に戻した

つまり、v2.1.268では解析できないコマンド行を拒否ルールにかける方向へ倒しましたが、time -p make buildのような正当なコマンドまで拒否されたため、v2.1.273で元のプロンプト確認に戻されています。

デフォルトモードでは「プロンプトに戻る」だけで済む

v2.1.273の挙動は「拒否ではなくプロンプトに戻す」というものです。つまりManualモード(デフォルト)では、evalやenv -Cで包んだコマンドでも実行前に確認プロンプトが出ます。ユーザーがそこで気づいて拒否すれば実害はありません。

問題が深刻化するのは、そのプロンプト自体を省略するモードで使ったときです。拒否ルールはbypassPermissionsを含むすべてのモードでブロックするのが原則ですが、Read/Editの拒否ルールがevalやenv -Cの中身を解析できず「一致なし」と判定されると、この原則が働かないままコマンドが素通りします。プロンプトを省略するモードでは、実行前に気づく機会そのものがなくなります。

# 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固有の欠陥というより、「コマンド文字列を静的解析して一致を取る」という拒否ルール全体の設計に由来します。Bashルールが一致しないコマンドの一覧を見ても、根は同じです。

ルール止まる止まらない
Bash(curl *)止まるcurl https://example.com止まらないsh -c 'curl https://example.com'
Bash(rm *)止まるrm -rf build/止まらないbash -c 'rm -rf build/'
Bash(git push *)止まるgit push origin main止まらないgit -C . push origin main

sh -cやbash -cで包む形は、evalやenv -Cと同じく「中身を渡すだけのラッパー」です。コマンド文字列に依存しない境界が必要なら、後述するサンドボックスがその役割を担います。

autoモードの分類器は拾える可能性があるが、bypassPermissionsには効かない

Claude Codeのアクション判定は4段階です。まず①permissions.allow/ask/denyルールに一致すればそこで即決し、一致しなければ②作業ディレクトリ内の読み取り専用操作は自動承認、③それ以外は分類器(auto modeの安全性レビュー)に回る、という順序です。

つまりevalやenv -Cで包んだコマンドが拒否ルールの①で「一致なし」と判定されても、autoモードならその先の③でもう一段、分類器によるレビューを受けます。分類器はコマンド文字列のパターンマッチではなく内容の意図を評価するため、拒否ルールをすり抜けた行を捕まえられる可能性があります。

一方bypassPermissionsモード(--dangerously-skip-permissions)には、この分類器という第二の網がそもそもありません。拒否ルールと分類器という2段のチェックが両方外れるため、evalやenv -Cで包んだコマンドはそのまま実行されます。この穴を塞ぐには、後述するサンドボックスやblockReadsOutsideWorkingDirectories、PreToolUseフックのように権限システムの外側にある仕組みを重ねる必要があります。自動運用でプロンプトを省きたいだけなら、bypassPermissionsではなくautoモードを選ぶことも対策の一つになります。

具体的な設定例 — 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で指定したパスへのアクセスはブロックされます。設定の詳細とallowReadでの例外の開け方はsandbox.filesystem.denyReadで認証情報ファイルを読ませないにまとめています。

PreToolUseフックを併用すると、Claude Codeの権限ルール評価とは別レイヤーでコマンド文字列そのものを検査できます。exit code 2で終了すれば、拒否ルールが一致しなくてもツール呼び出し自体を止められます。

#!/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

このフックは拒否ルールが検知できない行だけを狙い撃ちする簡易例ですが、正規表現である以上これも万能ではありません。最終的な境界としてはサンドボックスのファイルシステム制限を重ねるのが確実です。

対策 — permissions.denyだけに頼らずsandboxとhookを重ねる

permissions.denyはコマンドのテキストマッチである以上、evalやsh -cのようなラッパーを完全に塞ぎ切ることはできません。拒否ルールを唯一の防御にせず、OSレベルで境界を作る仕組みを重ねるのが確実な対策です。

  1. サンドボックスでファイル読み取りも制限する: sandbox.enabled: trueとsandbox.filesystem.denyReadを組み合わせれば、evalで中身を隠されても対象パスへの読み取りをOSレベルで止められます(設定例は前述)。
  2. permissions.blockReadsOutsideWorkingDirectoriesで作業ディレクトリ外の読み取りを塞ぐ: このオプションは本来、作業ディレクトリの外にあるファイルの読み取りをブロックするための設定です。シェルパーサーが追えないコマンド(サブシェルや複数回のcdを含む行など)は、autoモードやbypassPermissionsモードでもプロンプトに戻るフェイルセーフの側面もあります。ただし今回の./secrets/は作業ディレクトリの内側にあるため、このオプションが直接カバーする範囲ではありません。作業ディレクトリ内の秘密ファイルには、対策1のdenyReadかPreToolUseフックを重ねる必要があります。
  3. PreToolUseフックでコマンド文字列を自前検査する: フックはClaude Codeの権限ルール評価とは独立に動き、exit code 2でツール呼び出し自体を止められます。evalの引数やenv -Cのターゲットディレクトリを正規表現で検査するフックをBashツールに登録すれば、拒否ルールが素通りさせた行も別レイヤーで捕まえられます。
  4. --dangerously-skip-permissionsを隔離環境の外で使わない: このモードはコンテナやVMなど、Claude Codeが被害を出せない隔離環境でのみ使う設計です。ホストマシン上でこのモードを使うと、プロンプトという確認の網がなくなるため、evalによる素通りを止められるのはサンドボックスやPreToolUseフック、blockReadsOutsideWorkingDirectoriesといった権限システムの外側の仕組みだけになります。

permissions.denyはあくまで「Claudeが自分で書いたコマンドをどう扱うか」を制御する一次防波堤であり、任意のシェル構文を完全に解析するようには作られていません。この構造は既に/permissionsコマンドで権限ルールとauto mode拒否を管理するやallowManagedPermissionRulesOnlyで権限ルールを管理設定だけに縛るでも触れているとおりで、組織で拒否ルールを配布する場合ほどサンドボックス側の設定も合わせて確認する価値があります。

まとめ

permissions.denyのRead/Editルールは、evalやenv -Cのように中身を丸ごとシェルへ渡す構文を解析できません。v2.1.268でこの穴を塞ぐ修正が入りましたが、time -p make buildのような正当なコマンドまで拒否してしまう誤検知を起こしたため、v2.1.273でプロンプトに戻す挙動へリバートされ、v2.1.283時点でも再修正はありません。Manualモードならプロンプトで気づけますが、--dangerously-skip-permissionsのようにプロンプトを省略するモードで使うと、この穴がそのまま素通りになります。拒否ルールを唯一の防御にせず、サンドボックスのdenyReadやblockReadsOutsideWorkingDirectories、PreToolUseフックを重ねて設定しているかを確認してください。すでに「Read deny rule」エラーの原因を調べたことがある場合は、「Read deny rule」エラーの対処も合わせて確認すると挙動の違いを切り分けやすくなります。autoモード自体に独自の拒否・許可ルールを足したい場合はClaude CodeのautoModeに独自ルールを追加する書き方も参考になります。

この記事を共有:XはてブLinkedIn