CLAUDE LABEN
NEST — サブエージェントがさらにサブエージェントを起動できる深さの既定が1から3になりました。調査・実装・検証を分けた多段構成が追加設定なしで組めますAPISKILL — 同梱のclaude-api skillが既定でClaude Opus 5を使うようになりました。Opus 4.8からの移行手順も併記されていますENVVAR — Managed MCPのallowlist・denylistに書いた${VAR}が、設定ファイル内のenvではなく起動時環境とmanaged-settingsのenvから解決されますA11Y — セッションをスクリーンリーダーで追えるモードが加わりました。削除されたテキストの読み上げにも対応していますVOICE — 音声モードがOpus・Sonnet・Haikuのいずれでも動くようになり、GmailやSlackなど接続済みツールにも届きます。対応言語も増えましたTEACH — 7月14日に教育者向けのClaude for Teachersが発表されました。あわせてカナダのAI研究へ1,000万ドルの拠出が公表されていますNEST — サブエージェントがさらにサブエージェントを起動できる深さの既定が1から3になりました。調査・実装・検証を分けた多段構成が追加設定なしで組めますAPISKILL — 同梱のclaude-api skillが既定でClaude Opus 5を使うようになりました。Opus 4.8からの移行手順も併記されていますENVVAR — Managed MCPのallowlist・denylistに書いた${VAR}が、設定ファイル内のenvではなく起動時環境とmanaged-settingsのenvから解決されますA11Y — セッションをスクリーンリーダーで追えるモードが加わりました。削除されたテキストの読み上げにも対応していますVOICE — 音声モードがOpus・Sonnet・Haikuのいずれでも動くようになり、GmailやSlackなど接続済みツールにも届きます。対応言語も増えましたTEACH — 7月14日に教育者向けのClaude for Teachersが発表されました。あわせてカナダのAI研究へ1,000万ドルの拠出が公表されています
記事一覧/Claude Code
Claude Code/2026-07-27上級

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

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

Claude Code205MCP48権限設計自動化72

プレミアム記事

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

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

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

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

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

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

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

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

検証に使ったハーネス

実際の展開処理は 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 はセキュリティ制御ではないと明記しています。強制したいなら serverCommandserverUrl を書く、という前提はここでも変わりません。

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

この記事の続きを読む

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

この記事で得られること
同一の managed-settings.json が環境しだいで別のポリシーになる過程を、7サーバーの判定表として再現するハーネス一式(展開器・照合器・差分器)
未解決変数が allowlist では締めすぎ、denylist では緩みすぎに転ぶ非対称性の実測(同じパターンが 4/4 件一致と 0/4 件一致に分かれる)
実効ポリシーを sha256 の指紋に落として CI と手元の食い違いを起動前に落とす preflight スクリプト全文(実行 42ms・環境差で非ゼロ終了)
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

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

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

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

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

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

関連記事

Claude Code2026-05-23
Claude Code の Skill・Subagents・Rules を組み合わせて、社内プロジェクトを 1 時間以内に動かす:個人運用に落とし込んだ実装パターン
Claude Code の Skill・Subagents・Rules を組み合わせて、後回しになりがちな社内プロジェクトの 1 週間分のタスクを 1 時間で完了させる仕組み。SonicGarden の実装を起点に、個人開発で 12 年回してきた運用感覚に落とし込んだ構成と落とし所をまとめます。
Claude Code2026-05-06
Claude Code × MCP で構築する個人開発 AI 自動化ハブ — 複数サービス横断の設計と実装
Claude Code と MCP を組み合わせ、GitHub・Notion・Slack を横断する個人開発向けの AI 自動化ハブを設計・実装します。サービス間の文脈維持・エラーハンドリング付きコード例・トークンコスト削減の実測までまとめました。
Claude Code2026-05-04
Claude Code で MCP サーバーを実践活用する — 設計から運用まで
Claude Code の MCP サーバー連携を実際のプロジェクトで使い始める方法を、設定のコツから頻出エラーの対処法、実践的な構成例まで解説します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →