Windows desktop app silently fails to expand ${VAR} in user-scope mcpServers env block (CLI expands correctly)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Summary
${VAR} placeholders in the env block of a user-scope MCP server definition (~/.claude.json → mcpServers) are not expanded by the Windows desktop app — the MCP server process receives the literal string "${VAR}". The standalone CLI expands the same config correctly. The failure is completely silent: no error, no warning; the server starts normally and fails only at the authentication layer (in our case: Confluence treated the literal placeholder as an invalid bearer token and fell back to anonymous access).
This appears to be the same defect as #40372 (reported for the macOS desktop app with a project .mcp.json). That issue was auto-closed by the triage bot as a duplicate of #9427 — but #9427 is about plugin .mcp.json scope, which is a different code path. As a result there is currently no open canonical issue for the desktop-app case. Related family of CLI-vs-host expansion inconsistencies: #14032 (VS Code extension), #6204 (HTTP headers under WSL).
Environment
- Desktop app: Claude (Windows)
1.24012.1,C:\Users\<user>\AppData\Local\AnthropicClaude\app-1.24012.1\claude.exe - Standalone CLI (works): Claude Code
2.1.215, launched fromcmd.exe - OS: Windows 11 Pro 10.0.26200
- Config scope: user-scope
~/.claude.json→mcpServers.<name>.env
Reproduction
- Set a User-scope environment variable, e.g.
MY_SECRET=hello44chars..., then log off/on (clean env chain). - Define a user-scope stdio MCP server whose
envblock references it:
{
"mcpServers": {
"env-echo": {
"command": "node",
"args": ["C:\\repro\\env-echo-mcp.js"],
"env": {
"RESOLVED_VALUE": "${MY_SECRET}"
}
}
}
}
(env-echo-mcp.js = any minimal stdio MCP server exposing one tool that returns process.env.RESOLVED_VALUE.)
- Start the server from the CLI (
claudeincmd.exe), call the tool → returnshello44chars...✅ - Start the server from the Windows desktop app, call the tool → returns the literal string
${MY_SECRET}❌
Evidence that this is an expansion bug, not an environment-inheritance problem
We initially suspected the classic Windows stale-environment chain and ruled it out step by step:
- Full machine reboot with the variable already set User-scope → app-spawned server still received the literal placeholder.
- The app process demonstrably has the variable: shells spawned by the app inherit it (verified, correct 44-char value). There are no PowerShell profiles and no cmd AutoRun on the machine (verified), so the child-shell environment is pure inheritance from the app process — the variable is present in the app's environment, it just never gets substituted into the
envblock value. - The MCP server reads its env verbatim (verified in the server's source): it performs no
${VAR}self-expansion, so the spawning host must expand. - Same config, same machine, same user, same token — CLI works, desktop app doesn't. This isolates the defect to the desktop app's MCP spawn path.
Impact
Any user-scope MCP server that receives secrets via ${VAR} (the documented way to keep secrets out of config files) silently runs unauthenticated/broken in the desktop app, while working in the CLI. The silent failure makes this expensive to diagnose — the server connects fine and only misbehaves at the application-auth level.
Workaround we settled on: the MCP tool supports a <VAR>_FILE convention, so we point it at an ACL-protected file instead of relying on expansion. Hardcoding the literal secret in .claude.json also works but puts plaintext secrets on disk.
Suggested actions
- Fix
${VAR}expansion in the desktop app's MCP server spawn path (user-scope.claude.json, and per #40372 also project.mcp.json). - Emit a warning when a
${VAR}reference cannot be resolved instead of passing it through silently. - Re-open or re-triage #40372 — it was closed as a duplicate of #9427, which covers plugin scope, not the desktop app.
What Should Happen?
Per the MCP docs, ${VAR} expansion in env blocks should behave identically regardless of host (CLI, desktop app, IDE extension). At minimum, a failed/skipped expansion should produce a visible warning instead of silently passing the placeholder through.
Error Messages/Logs
Steps to Reproduce
- Set a User-scope environment variable, e.g.
MY_SECRET=hello44chars..., then log off/on (clean env chain). - Define a user-scope stdio MCP server whose
envblock references it:
{
"mcpServers": {
"env-echo": {
"command": "node",
"args": ["C:\\repro\\env-echo-mcp.js"],
"env": {
"RESOLVED_VALUE": "${MY_SECRET}"
}
}
}
}
(env-echo-mcp.js = any minimal stdio MCP server exposing one tool that returns process.env.RESOLVED_VALUE.)
- Start the server from the CLI (
claudeincmd.exe), call the tool → returnshello44chars...✅ - Start the server from the Windows desktop app, call the tool → returns the literal string
${MY_SECRET}❌
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.215
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Windows Terminal
Additional Information
_No response_
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗