◉CLAUDE LABEN
●2.1.310 — Bedrock・Vertex・Foundry でも Monitor ツールが使え、ゲートウェイの失敗を修正しました(10/23)●2.1.307 — スリープやキーチェーンロックのあとに MCP が再接続されない問題を修正しました(10/21)。2.1.306 では claude -p の誤表示も解消●SONNET 4.5 — Claude API での提供終了は 11/30、残り36日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●API — 新しい API バージョン 2026-10-21 では temperature・top_p・top_k を渡すと 400 になります●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.310 — Bedrock・Vertex・Foundry でも Monitor ツールが使え、ゲートウェイの失敗を修正しました(10/23)●2.1.307 — スリープやキーチェーンロックのあとに MCP が再接続されない問題を修正しました(10/21)。2.1.306 では claude -p の誤表示も解消●SONNET 4.5 — Claude API での提供終了は 11/30、残り36日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●API — 新しい API バージョン 2026-10-21 では temperature・top_p・top_k を渡すと 400 になります●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/Claude Code
⟐ Claude Code/2026-07-27上級

許可リストに書いた環境変数は誰の環境で展開されるのか — Managed MCP の実効ポリシーを指紋で確かめる

Managed MCP の allowlist・denylist に書いた環境変数参照の解決元が、起動時環境と managed-settings の env に変わりました。同じ設定ファイルが環境ごとに別のポリシーになる仕組みを手元で再現し、実効ポリシーを指紋で突き合わせる起動前チェックを組みました。

Claude Code275MCP59権限設計7自動化87

✦ プレミアム記事

変更点はたった1行でした。Managed MCP の allowlist・denylist に書いた ${VAR} の解決元が、設定ファイル内の env から、起動時の環境と managed-settings の env に変わった、という記述です。

それを読んで手が止まりました。私が個人開発の運用で組んでいる自動化は、呼び出しごとに環境変数が引き継がれない前提で書いてあります。つまり「起動時の環境」は、私にとって最も安定しない変数なのです。そこが解決元になったということは、ポリシーの意味が呼び出しごとに変わりうる、ということでもあります。

許可リストは、書いた内容が守られている前提でしか安心を提供しません。前提が環境に依存するのなら、ファイルを眺めるだけでは何も確認できていないことになります。そう思い直して、両方の解決順序を手元で再現し、判定がどこでずれるかを測りました。

解決元が「ファイルの内側」から「外側」へ出た

以前の挙動では、${VAR} は設定ファイル自身の env ブロックから解決されていました。ファイルを閉じた世界で完結していたわけです。配布した JSON を読めば、展開後の値も読み取れました。

新しい挙動では、まず起動時の環境が参照され、次に managed-settings の env が参照されます。組織で配布する設定を CI と手元の端末で切り替えやすくする、という意図は理解できます。ただし同時に、ポリシーの内容を決める入力が、管理者の手を離れた場所に移りました。

公式ドキュメントは、この構造をはっきり書いています。serverCommand と serverUrl は、ポリシー側の記述とサーバー側の設定値の両方が同じ展開を通ってから照合されます。そして「${VAR} の展開は Claude Code 自身のプロセス環境を読むため、変数を参照する serverCommand や serverUrl のエントリは、利用者が設定した値に展開される。強制のために頼るエントリはリテラルで書くこと」と注意されています。

読めば当然の話です。ただ、私が実際に測ってみて驚いたのは、この一文の帰結が想像よりずっと広かったことでした。

検証に使ったハーネス

実際の展開処理は Claude Code 側の実装なので、ここでは文書化されている規則に沿って手元で再現しました。以下は展開器です。解決元を配列で渡し、先に来たものを優先します。旧挙動と新挙動を、この配列の並べ替えだけで切り替えられるようにしてあります。

// expand.mjs — ${VAR} / ${VAR:-default} の展開を、解決元の順序を差し替えられる形で再現する
const REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
 
// sources: 先に来た方を優先する [name, map] の配列
export function expand(value, sources, opts = {}) {
  const misses = [];
  const out = String(value).replace(REF, (whole, name, dflt) => {
    for (const [origin, map] of sources) {
      if (map && Object.prototype.hasOwnProperty.call(map, name) && map[name] !== undefined) {
        return String(map[name]);
      }
    }
    if (dflt !== undefined) { misses.push({ name, kind: "default", used: dflt }); return dflt; }
    misses.push({ name, kind: "unresolved", used: "" });
    return "";
  });
  return { out, misses };
}
 
export function expandEntry(entry, sources, opts) {
  const misses = [];
  const push = r => { misses.push(...r.misses); return r.out; };
  if (entry.serverUrl !== undefined)
    return { entry: { serverUrl: push(expand(entry.serverUrl, sources, opts)) }, misses };
  if (entry.serverCommand !== undefined)
    return { entry: { serverCommand: entry.serverCommand.map(a => push(expand(a, sources, opts))) }, misses };
  return { entry: { ...entry }, misses }; // serverName は展開しない
}

照合側は、ドキュメントに書かれた規則をそのまま写しました。URL は * をどこにでも置けます。ホスト名は大文字小文字を区別せず、末尾の FQDN ドットを無視します。パスは区別し、パスを持たないパターンは全パスに一致します。コマンドは引数の順序まで含めた完全一致です。

// match.mjs — URL ワイルドカード照合とコマンド完全一致、および評価順序
function normHost(h) { return h.replace(/\.$/, "").toLowerCase(); }
 
function splitUrl(u) {
  const m = /^([^:]*):\/\/([^/?#]*)(.*)$/.exec(u);
  return m ? { scheme: m[1], host: m[2], path: m[3] } : null;
}
 
function globToRe(s, { ci = false } = {}) {
  const body = s.split("*").map(p => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
  return new RegExp("^" + body + "$", ci ? "i" : "");
}
 
export function urlMatches(pattern, url) {
  const p = splitUrl(pattern), u = splitUrl(url);
  if (!p || !u) return globToRe(pattern, { ci: true }).test(url);
  if (!globToRe(p.scheme, { ci: true }).test(u.scheme)) return false;
  if (!globToRe(normHost(p.host), { ci: true }).test(normHost(u.host))) return false;
  if (p.path === "") return true;                 // パスなしのパターンは全パスに一致
  return globToRe(p.path).test(u.path === "" ? "/" : u.path);
}
 
export function commandMatches(pattern, cmd) {
  return Array.isArray(pattern) && Array.isArray(cmd)
    && pattern.length === cmd.length
    && pattern.every((a, i) => a === cmd[i]);
}
 
function hit(entry, server) {
  if (entry.serverUrl !== undefined)
    return server.type === "remote" && urlMatches(entry.serverUrl, server.url);
  if (entry.serverCommand !== undefined)
    return server.type === "stdio" && commandMatches(entry.serverCommand, server.command);
  if (entry.serverName !== undefined) return entry.serverName === server.name;
  return false;
}
 
// allow が undefined なら無制限、[] なら全拒否
export function evaluate(server, { allow, deny = [] }) {
  for (const e of deny) if (hit(e, server)) return { allowed: false, reason: "denylist" };
  if (allow === undefined) return { allowed: true, reason: "no-allowlist" };
  const hasUrl = allow.some(e => e.serverUrl !== undefined);
  const hasCmd = allow.some(e => e.serverCommand !== undefined);
  for (const e of allow) {
    if (!hit(e, server)) continue;
    if (e.serverName !== undefined) {
      if (server.type === "remote" && hasUrl) continue;   // URL 項目があると名前一致は数えない
      if (server.type === "stdio" && hasCmd) continue;
    }
    return { allowed: true, reason: Object.keys(e)[0] };
  }
  return { allowed: false, reason: "not-in-allowlist" };
}

serverName を展開しないのは仕様どおりですが、実装しながら落とし穴の位置を改めて意識しました。名前は利用者が付けるラベルであって、サーバーの実体ではありません。ドキュメントも serverName はセキュリティ制御ではないと明記しています。強制したいなら serverCommand か serverUrl を書く、という前提はここでも変わりません。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦同一の managed-settings.json が環境しだいで別のポリシーになる過程を、7サーバーの判定表として再現するハーネス一式(展開器・照合器・差分器)
✦未解決変数が allowlist では締めすぎ、denylist では緩みすぎに転ぶ非対称性の実測(同じパターンが 4/4 件一致と 0/4 件一致に分かれる)
✦実効ポリシーを sha256 の指紋に落とす preflight スクリプト全文と、その指紋が見落としていた2件(強制フラグと serverName の欠落)の実測・修正版(10回平均 40ms)
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⟐ Claude Code2026-10-23
夜間の claude -p で MCP が「認証待ち」になった朝、sleep と keychain を切り分ける事前確認を足しました
Claude Code 2.1.307 は macOS の sleep とログインキーチェーンのロックが原因の MCP 失敗を修正しました。修正の有無に関わらず効く事前確認スクリプトと、sleep 履歴との照合手順をまとめます。
⟐ Claude Code2026-10-23
夜間の claude -p で MCP が「認証が必要」と出る朝に、Mac のスリープとキーチェーンを疑う順番
無人で回した claude -p の朝のログに「needs authentication」や MCP のタイムアウトが残っているとき、Mac のスリープとログインキーチェーンのロックを先に確かめる手順です。v2.1.307 で変わった挙動と、版を上げても残る部分の線引きをまとめました。
⟐ Claude Code2026-10-19
MCP の allow を `mcp__サーバー名` だけで書いていた人へ — v2.1.299 の変更後に見直す三つの行
Claude Code v2.1.299 で MCP の allow ルールが「名指ししたサーバーだけ」に一致する形へ変わりました。settings.json の古い書き方を点検する手順と、動く Python の点検スクリプトを載せています。