Claude Codeの許可ルールの*は何を通す?2.1.323の警告と点検

17 分で読める
SaaSClaude Code

Bash(npx *) は、npxが実行する内側のコマンドを限定しません。* が空白を含む文字列に一致するためです。必要なコマンドまで指定して許可を絞ってください。Claude Code 2.1.323では、変数の値に*がある許可への起動時警告が追加され、NODE_ENV*などの判定も修正されました。今回、偽APIと偽コマンドで2.1.322と2.1.323を比較しました。

確認日:2026-11-13 JST。対象:Claude Code 2.1.322・2.1.323、macOS 27.0、Node.js 26.12.1。公式文書も同日に取得。ローカルの偽Messages APIが固定のtool_useを返す試験で、実モデルの応答やAPI料金を測ったものではありません。

要点:

  • *の前に書くプログラム・サブコマンドで、許可する範囲を絞ります。
  • npxやdevbox runは内側のコマンドまで書きます。許可ルールはsandboxの代わりにはなりません。
  • Bash(NODE_ENV*)は今回、2.1.322では実行、2.1.323では拒否になりました。
  • 点検例は設定を書き換えません。出力はallowの配列番号と分類で、ルール本文も伏せます。

Bash(npx *)を許可すると何が確認なしで動きますか?

*はどこまでの文字に当たりますか?

公式のPermissionsでは、*は空白を含む任意の文字列に一致します。末尾の唯一の *は、引数なしのコマンドにも一致します。Bash(ls:*)はBash(ls *)と同じ末尾ワイルドカードです。

ルール 公式仕様での範囲 今回の観測(両版)
npx全体: Bash(npx *) npxの後ろを限定しない(公式規則からの帰結) 偽npxのprettier・mytool・引数なしが実行
prettier限定: Bash(npx prettier *) prettierを指定 prettierは実行、mytoolは拒否
空白なし: Bash(deploy*) 空白なしでプログラム名にも一致 deploy-prodとdeployxも実行
空白あり: Bash(deploy *) deployとその引数 deployは実行、deploy-prodは拒否
旧形式の末尾: Bash(deploy:*) deployの末尾ワイルドカード deploy xは実行、deploy-prodは拒否
lsを指定: Bash(ls *) lsに一致、lsofには不一致 lsofも実行。読み取り専用の別経路あり
lsofにも一致: Bash(ls*) lsofにも一致 lsofが実行。一致経路はこの観測で特定できない

なぜnpxやdevbox runの内側まで書きますか?

Wrappersには、許可判定の前に取り除くwrapperの一覧があります。npx・devbox run・docker execなどはその一覧に含まれません。これらは引数のコマンドを実行するため、広いルールでは内側の実行内容を絞れません。今回の偽npxは引数をmarkerへ記録するだけで、パッケージのダウンロードや内側のプログラムの実行はしていません。

Bash(ls*)とBash(ls *)は何が違いますか?

空白の有無がプログラム名の境界を変えます。ls*はlsofにも一致する一方、ls *はlsを指定します。ただしClaude Codeには読み取り専用コマンドを確認なしで実行する判定もあります。今回、allowなしでも偽lsofが実行されました。ルールに一致したかを調べる負の対照には、deploy-prodのような読み取り専用でない偽コマンドを使いました。

2.1.323で何が直りましたか?

2.1.323の変更履歴は、空白なしのワイルドカードが一部で許可確認を飛ばす問題を修正したと説明しています。今回deploy*の行には版による差がありませんでした。差を観測したのは、変数名だけを指定したBash(NODE_ENV*)です。

allowと偽コマンド 2.1.322 2.1.323
変数名: Bash(NODE_ENV*) → NODE_ENV=test mytool 実行 許可拒否
空白なし: Bash(deploy*) → deploy-prod 実行 実行
具体値: Bash(PORT=3000 npm run dev) → 同じコマンド 実行 実行
同じ具体値ルール → PORT=4000 npm run dev 許可拒否 許可拒否

起動時の警告はどんなルールに出ますか?

今回の警告は--debug-fileのログで確認しました。JSON出力のstderrは空でした。画面の警告表示自体は実測していません。

allow 2.1.322のdebug 2.1.323のdebug 警告の要旨
変数の値: Bash(PORT=* npm run dev) 該当警告なし 警告あり 変数の値の*は空白も取り込む
サブコマンド前: Bash(git * main) 警告あり 警告あり 後ろの語までの間にオプションも入る
具体値: Bash(PORT=3000 npm run dev) 該当警告なし 該当警告なし 今回の具体値では警告なし

PORT=*の警告はどう消しますか?

必要な値に固定して、Bash(PORT=3000 npm run dev)のようにします。*には空白が含まれるので、値の範囲だけを表すつもりで使わないでください。警告を止める専用設定は確認していません。元の広いルールも残したままにすると、具体的なルールだけを追加しても許可は絞れません。

git * mainはなぜ警告されますか?

*の位置にはサブコマンドやオプションも入ります。公式文書はgitの-cなどによる別プログラムの実行にも注意を促しています。Bash(git log *)など、必要なサブコマンドの後ろへ*を置きます。今回は偽gitで警告と許可だけを観測し、本物のgit設定を変更するコマンドは実行していません。

&&やパイプでつないだコマンドはどう判定されますか?

Compound commandsでは、全サブコマンドがそれぞれ許可される必要があります。今回Bash(mytool *)だけでは、mytool a && deploy-prod・パイプ・セミコロンの3形とも拒否され、両コマンドのmarkerは出ませんでした。末尾のmytool a &&は構文エラーで実行されませんでした。deny・askは一致するサブコマンドが一つでもある場合に適用されます。

自分のsettingsを安全に点検するには?

点検スクリプトを保存し、確認したいsettings JSONだけを自分で指定します。自動でホームを探索しません。JSONを読み、permissions.allowだけを分類します。設定の書き込み・通信・子プロセス・Claude Codeの起動はありません。

node permission-wildcard-audit.mjs /path/to/settings.json

出力のindexはallow配列の0始まりの番号です。秘密は=の後ろ以外にも入り得るため、計画時の部分マスクから変更し、ルール本文を一律にBash([omitted])などへ伏せました。元ファイルの該当行は自分のエディタで確認してください。構文だけの簡易分類であり、okは安全性の保証ではありません。たとえばgit log * mainのオプションまで精査しません。JSONでない入力、allow配列がない設定、65,536 bytesを超すファイルやファイル自体のsymlinkは拒否します。親ディレクトリのsymlinkと読み取り途中の差し替えは防いでいません。

import { lstatSync, readFileSync, realpathSync } from 'node:fs'
import { fileURLToPath } from 'node:url'

// Heuristic review labels. An 'ok' label is not a security approval.
export function classify(rule) {
  if (typeof rule !== 'string') throw Error('allow entries must be strings')
  if (rule === 'Bash' || rule === 'Bash(*)') return 'bare-tool'
  const m = /^Bash\((.*)\)$/.exec(rule)
  if (!m) return 'other-tool'
  // The documented trailing :* form has the same boundary as a trailing space + *.
  const p = m[1].endsWith(':*') ? `${m[1].slice(0, -2)} *` : m[1]
  if (!p.includes('*')) return 'ok'
  if (/^(?:[A-Za-z_][\w]*=[^ ]*\*[^ ]*\s*)+/.test(p)) return 'variable-wildcard'
  if (
    /^(?:npx|bunx|pnpm dlx|devbox run|mise exec|direnv exec|docker exec|bash -c|sh -c|env|xargs\s+-\S+)(?:\s|$)/.test(
      p,
    )
  )
    return 'runner-wildcard'
  if (/[^\s:]\*/.test(p)) return 'attached-wildcard'
  if (/^\S+\s+\*\s+\S/.test(p)) return 'wildcard-before-subcommand'
  if (/^\S+ \*$/.test(p) && p !== 'ls *') return 'program-only-wildcard'
  return 'ok'
}

export function inspect(text) {
  const data = JSON.parse(text)
  if (!data || typeof data !== 'object' || Array.isArray(data))
    throw Error('settings must be an object')
  if (!data.permissions || !Array.isArray(data.permissions.allow))
    throw Error('permissions.allow must be an array')
  return data.permissions.allow.map((rule, index) => ({
    index,
    // Never echo user-supplied rule text: credentials can also appear without '='.
    rule: /^Bash(?:\(|$)/.test(rule) ? 'Bash([omitted])' : '[omitted]',
    classification: classify(rule),
  }))
}

export function audit(path) {
  const stat = lstatSync(path)
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 65536)
    throw Error('regular file up to 65,536 bytes required')
  return inspect(readFileSync(path, 'utf8'))
}

function startedDirectly() {
  // Why real paths on both sides: Node may resolve links in import.meta.url while argv[1] stays
  // as typed. Compared as written, a start through a link printed nothing and exited 0.
  try {
    return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
  } catch {
    return false // argv[1] names no file (stdin, -e), so this module was imported.
  }
}
if (startedDirectly()) {
  try {
    if (process.argv.length !== 3)
      throw Error('usage: node permission-wildcard-audit.mjs <settings.json>')
    console.log(JSON.stringify(audit(process.argv[2]), null, 2))
  } catch {
    // JSON parser error text can include secrets. Print only a constant message.
    console.error(
      'Cannot audit: pass valid settings JSON with a string array permissions.allow (regular file, <=65,536 bytes).',
    )
    process.exitCode = 1
  }
}

許可を絞るにはどう書き直しますか?

次は書き換え候補です。具体的なルールにも、実行対象のコードや引数の意味を確かめる必要があります。引数位置に*があれば、その位置の危険なオプションも検討してください。

広い形 絞った候補 今回の確認
npx全体: Bash(npx *) Bash(npx prettier --check *) --checkは実行、--writeは拒否
git全体: Bash(git *) Bash(git log *)・Bash(git diff *) 公式の絞り方。実gitでは未実測
lsofにも一致: Bash(ls*) lsを指定: Bash(ls *) 境界は公式仕様。lsofの実行だけでは判別不能
変数の値: Bash(PORT=* npm run dev) 具体値: Bash(PORT=3000 npm run dev) 3000は実行、4000は拒否
内側を限定しないrunner: Bash(devbox run *) Bash(devbox run npm test) npm testは実行、mytoolは拒否

許可ルールだけで書き込み先を守る方法は、作業フォルダ外への書き込み対策で範囲を確かめてください。独自の実行前判定はHooksの終了コードも参照できます。

実測ではどうなりましたか?

各版39条件、計78条件を別プロセスで実行しました。設定なしの対照、未信頼projectのallow、変数・空白・runner・複合コマンド・denyを含みます。全条件の結果をJSONで配布しています。実行はmarkerと成功tool_result、拒否はmarkerなしと拒否tool_resultの両方で判断しました。構文エラーは別に記録しました。

未信頼projectに置いたallowは、両版でmytoolを許可しませんでした。subagent inline MCPとフォルダ信頼でも、設定の出所と信頼の違いを扱っています。MCP自体の設定はClaude Code MCP設定を参照してください。

どう再現しますか?

  1. 偽データの空フォルダを作り、env -iで引き継ぐ環境変数を限定します。
  2. CLAUDE_CONFIG_DIR・CODEX_HOME・HOME・TMPDIR・XDG・Git設定をすべてscratchへ向けます。実設定への読み書きと外向き通信をmacOS sandboxで拒否し、scratch内のカナリアで拒否が効くことを確かめます。
  3. localhostの偽Messages APIへダミーキーで接続し、固定のBash tool_useを返します。PATHの偽コマンドはmarkerへの記録だけを行います。
  4. 各条件で設定フォルダを作り直し、claude -p "run the scripted command" --permission-mode defaultを実行します。marker・tool_result・debugを照合します。

npmで取得した2.1.323のtarballはregistryのintegrityと照合しました。CLIが表示するusageやcostは偽レスポンスからの計算で、実APIの利用量・課金ではありません。自動更新と非必須通信は無効にしました。拒否を外したカナリア、tool_useを返さない偽API、markerを無視する判定器を負の対照に使います。

確認していないことは何ですか?

実API・本物のnpxによるダウンロード・sudo・ask・全OS・全バージョン・対話画面は未検証です。実モデルがどのコマンドを選ぶかや、パッケージ内コードの安全性も測っていません。許可ルールはシェル全体のセキュリティ境界ではなく、別の書き方や実行ファイルのパスまで同一視しない制限があります。検索サジェスト、自然検索流入、AI引用の増減は未取得です。

関連記事と一次情報はどこですか?

よくある質問は?

Bash(npx *)は安全ですか?

npxが実行する内側のコマンドを限定しない広い許可です。必要なコマンドと引数まで指定してください。シェルの複合コマンドや他の許可判定まで一律に通す意味ではありません。

Bash(ls*)とBash(ls *)はどちらを使えばよいですか?

lsを指定したいなら、空白のあるBash(ls *)にします。公式仕様では空白なしはlsofにも一致します。ただしlsofは読み取り専用として実行される場合があり、実行された事実だけでルールへの一致を証明できません。

PORT=*の起動時警告を消すにはどうしますか?

必要な値を明示したBash(PORT=3000 npm run dev)などへ絞ります。警告を無効にする設定は今回確認していません。書き換え後も自分のコマンドで判定を確かめてください。

&&でつないだコマンドは一つのルールで許可されますか?

各サブコマンドに許可が必要です。今回、mytoolだけのallowでは後ろのdeploy-prodが拒否されました。末尾の&&は構文エラーになり、許可への一致とは区別しています。

denyルールはsudoやenv経由にも効きますか?

2.1.323の変更履歴にはsudoやenvなどを経由したdeny・askの取りこぼし修正があります。今回の合成試験ではenvとtimeoutのdenyを両版で確認しました。sudoとaskの実測はしていません。

2.1.322以前ではどうすればよいですか?

公式の更新手順で更新し、広い許可を点検してください。今回の旧版実測は2.1.322だけです。NODE_ENV*が確認を飛ばす差は観測しましたが、過去の全バージョンや全コマンドを検査したものではありません。

このトピックを深掘りする

SaaSの導入・連携

AIに仕事の続きを渡すために、会議の原文、未完了の宿題、確認できない項目をどう残すか。導入判断と連携実装を掘り下げる。

全21本中 1 本目。

このトピックの記事一覧

シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →

よくある質問

Bash(npx *)は安全ですか?

npxが実行する内側のコマンドを限定しない広い許可です。必要なコマンドと引数まで指定してください。シェルの複合コマンドや他の許可判定まで一律に通す意味ではありません。

Bash(ls*)とBash(ls *)はどちらを使えばよいですか?

lsを指定したいなら、空白のあるBash(ls *)にします。公式仕様では空白なしはlsofにも一致します。ただしlsofは読み取り専用として実行される場合があり、実行された事実だけでルールへの一致を証明できません。

PORT=*の起動時警告を消すにはどうしますか?

必要な値を明示したBash(PORT=3000 npm run dev)などへ絞ります。警告を無効にする設定は今回確認していません。書き換え後も自分のコマンドで判定を確かめてください。

&&でつないだコマンドは一つのルールで許可されますか?

各サブコマンドに許可が必要です。今回、mytoolだけのallowでは後ろのdeploy-prodが拒否されました。末尾の&&は構文エラーになり、許可への一致とは区別しています。

denyルールはsudoやenv経由にも効きますか?

2.1.323の変更履歴にはsudoやenvなどを経由したdeny・askの取りこぼし修正があります。今回の合成試験ではenvとtimeoutのdenyを両版で確認しました。sudoとaskの実測はしていません。

2.1.322以前ではどうすればよいですか?

公式の更新手順で更新し、広い許可を点検してください。今回の旧版実測は2.1.322だけです。NODE_ENV*が確認を飛ばす差は観測しましたが、過去の全バージョンや全コマンドを検査したものではありません。