Windows desktop app silently fails to expand ${VAR} in user-scope mcpServers env block (CLI expands correctly)

Status Fixed / completed
Reported on v2.1.215
Maintainer reply None cached
Activity 1 comment · opened Jul 22, 2026 · closed Aug 25, 2026

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.jsonmcpServers) 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 from cmd.exe
  • OS: Windows 11 Pro 10.0.26200
  • Config scope: user-scope ~/.claude.jsonmcpServers.<name>.env

Reproduction

  1. Set a User-scope environment variable, e.g. MY_SECRET=hello44chars..., then log off/on (clean env chain).
  2. Define a user-scope stdio MCP server whose env block 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.)

  1. Start the server from the CLI (claude in cmd.exe), call the tool → returns hello44chars...
  2. 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:

  1. Full machine reboot with the variable already set User-scope → app-spawned server still received the literal placeholder.
  2. 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 env block value.
  3. 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.
  4. 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

  1. Set a User-scope environment variable, e.g. MY_SECRET=hello44chars..., then log off/on (clean env chain).
  2. Define a user-scope stdio MCP server whose env block 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.)

  1. Start the server from the CLI (claude in cmd.exe), call the tool → returns hello44chars...
  2. 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_

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗