[BUG] Cowork silently blocks plugin-bundled MCP `env` var expansion for any non-built-in name, contradicting documented `${VAR}` custom-secret support
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet (closest related: #9427, #11927, #40372, #39125, #40747, #94321 — see Additional Information, none describe this specific mechanism)
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
A plugin-bundled MCP server's .mcp.json (or the equivalent inline mcpServers block in plugin.json — the docs say these are equivalent) declares a custom secret using the documented ${VAR} placeholder syntax:
{
"mcpServers": {
"testmo": {
"command": "npx",
"args": ["-y", "--@synapsestudios:registry=https://registry.npmjs.org/", "@synapsestudios/testmo-mcp-server@latest"],
"env": {
"TESTMO_API_TOKEN": "${TESTMO_API_TOKEN}",
"TESTMO_BASE_URL": "${TESTMO_BASE_URL}"
}
}
}
}
This is exactly the pattern documented at https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcp-json, which gives "Authorization": "Bearer ${API_KEY}" as its own canonical example and introduces the feature as supporting "sensitive values like API keys."
In Cowork (the agentic mode in the Claude Desktop app), this placeholder is never expanded — regardless of whether the variable is actually set, and regardless of how it's supplied (OS-level environment variable, ~/.claude/settings.json, or otherwise). The app's own log shows this is a deliberate, name-based gate, not a "variable not set" fallback:
[PluginMcpHostConfig] Plugin "..." server "testmo": config references environment variables
outside the MCP stdio safelist: TESTMO_API_TOKEN, TESTMO_BASE_URL — left unexpanded
(the desktop host does not expose arbitrary host env to plugin servers)
This is logged at plugin-config-load time (immediately after [LocalPluginsReader] Found N enabled local plugins), before any attempt to spawn the server or resolve values from the environment — it's a static check against the variable names in the config, not a runtime "is this in process.env?" check. We confirmed this distinction directly: setting TESTMO_API_TOKEN/TESTMO_BASE_URL via OS-level environment variables, via ~/.claude/settings.json, and across full app restarts made no difference — the identical log line was produced every time. This also isn't the documented "unset variable" behavior (code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcp-json's "reports a missing-variable warning... uses the unexpanded ${VAR} text as-is") — that's a different code path (reported via claude mcp list), with different wording, and doesn't explain a warning that fires identically whether or not the variable is set.
The only names that appear to be exempt from this gate are the built-in plugin placeholders (${CLAUDE_PROJECT_DIR}, ${CLAUDE_PLUGIN_DATA}, ${CLAUDE_PLUGIN_ROOT}) — any other name, including a documented custom-secret pattern like ${API_KEY}, is silently left as literal text.
Net effect: it is currently impossible for any plugin author to ship a plugin-bundled MCP server that needs a custom credential and have it work in Cowork — no matter which supported mechanism (OS env, project/user settings.json) is used to supply the value. There is also no error surfaced to the plugin author or the end user anywhere in the UI; the only trace is a [warn]-level line in main.log that nobody would think to check unless already deep into debugging a silent auth failure.
What Should Happen?
At minimum one of:
${VAR}expansion for plugin-bundled MCP servers in Cowork should work for arbitrary variable names, matching what's documented at the link above (and matching how it already works in Code) — or- If restricting plugin-bundled servers to a fixed safelist of names is intentional (plausible, as a defense against a plugin harvesting arbitrary host secrets), that restriction and the actual allowed name-list need to be documented, and a real, supported path for custom secrets needs to exist for plugin-marketplace-format plugins specifically (the existing
user_config/sensitivemechanism is documented only for the separate Desktop Extension.mcpbformat, and is itself reportedly broken in Cowork per #39125) — or - At the very least, this should surface as a visible, actionable error to the plugin author/user (e.g. in
claude mcp list, or an in-app notice), not only a[warn]-level internal log line.
Error Messages/Logs
2026-08-10 19:15:47 [warn] [PluginMcpHostConfig] Plugin "plugin_01WKRAPZXNyF6R8wVfqtnqpJ" server "testmo": config references environment variables outside the MCP stdio safelist: TESTMO_API_TOKEN, TESTMO_BASE_URL — left unexpanded (the desktop host does not expose arbitrary host env to plugin servers)
First observed 2026-08-10, persisted unchanged across an app update (1.26832.0 → 1.28929.0) and every subsequent app restart through at least 2026-08-26.
Steps to Reproduce
- Build a plugin with a bundled
.mcp.json(or inlinemcpServersinplugin.json) declaring a stdio MCP server whoseenvblock uses a custom${VAR}placeholder for a secret (not one of the built-in${CLAUDE_PROJECT_DIR}/${CLAUDE_PLUGIN_DATA}/${CLAUDE_PLUGIN_ROOT}placeholders). - Set that variable via any supported mechanism — OS-level environment variable,
~/.claude/settings.jsonenvblock, etc. — and fully restart the app. - Install/enable the plugin and start a Cowork session.
- Call a tool from the bundled MCP server that needs the credential — it fails (the server receives the literal string
${VAR}, not the value). - Check
%APPDATA%\Claude\logs\main.log— the[PluginMcpHostConfig]warning above appears at app startup, regardless of step 2.
Claude Model
None (occurs before any model interaction — this is plugin/MCP host configuration loading)
Is this a regression?
Unknown — first observed 2026-08-10 (earliest available log retention), so we can't confirm whether it ever worked differently before that.
Last Working Version
_No response_
Claude Code Version
App versions 1.26832.0 through 1.28929.0 (bug persisted across this update)
Platform
Claude Desktop / Cowork
Operating System
Windows 11 Home (10.0.26200)
Terminal/Shell
N/A (Cowork, not a terminal session)
Additional Information
Related issues checked and ruled out as not covering this specific mechanism:
- #9427 — "env variable expansion not working in plugin .mcp.json" (Claude Code CLI, inconsistent/version-dependent, no safelist identified, no Cowork angle)
- #11927 — "'env' vars from .claude/settings.json not passed to the plugins/mcps" (Claude Code CLI, project-scope settings.json propagation regression, affects arbitrary variable names — not a name-based safelist)
- #40372 — same general symptom class as #9427, Code-side, no maintainer explanation
- #39125 — "Cowork sessions don't inject user_config env vars into MCP server processes" — a related gap in the other supported secrets mechanism (Desktop Extension
.mcpbmanifests'user_config/sensitive), also broken in Cowork, but a different plugin format and a different failure mode (values entirely absent vs. present-but-unexpanded-with-explicit-safelist-warning) - #40747 — "Org-managed plugins have no way to inject secrets into MCP servers" (Cowork cloud sandbox) — same underlying pain point, but the reporter didn't identify a name-based safelist as the cause
- #94321 — "Plugin MCP OAuth is per-session and not durable; Cowork sessions can never authenticate one at all" — same category (plugin MCP auth broken in Cowork) but a different specific mechanism (OAuth session durability, not env var expansion)
- #90101 — a classic (non-Cowork) Desktop
claude_desktop_config.jsonbug, closedinvalid/"doesn't seem related to Claude Code" — confirms plain classic-Desktop bugs get rejected here, but this report is specifically about Cowork (labeledarea:coworkelsewhere in this tracker), not classic Desktop, so should be in scope
Happy to provide the full main.log excerpt or test further against a minimal repro plugin if useful.