[BUG] Desktop: ${VAR} expansion for MCP env is non-deterministic within one app instance (Linux; not the macOS launchd-env cause of #70486)

Status Open
Reported on v2.1.219
Maintainer reply None cached
Activity 0 comments · opened Aug 5, 2026

Preflight Checklist

  • [x] I have searched existing issues — this is not a duplicate of #9427 / #40372 / #70486; see "Why this is not those issues" below
  • [x] This is a single bug report
  • [x] I am using the latest version

What's Wrong?

On Linux, ${VAR} expansion for MCP server env blocks is non-deterministic within a single desktop-app process tree. Two MCP servers spawned by the same app instance, 41 seconds apart, received different values for the same key: one got the resolved secret, the other got the literal ${JIRA_API_TOKEN}.

The server that receives the literal connects successfully and then talks to the upstream API anonymously, so there is no startup error — the failure only surfaces later as confusing auth errors from the remote service (Client must be authenticated to access this resource, plus a localized "project not found" message in a language unrelated to the user's locale).

Why this is not #9427 / #40372 / #70486

Those reports are macOS-specific and attribute the root cause to GUI-app environment inheritance — "macOS GUI apps inherit the launchd environment, not the shell's", so the variable simply is not present when the app expands ${}. #70486 was closed NOT_PLANNED as a duplicate; #40372 was auto-closed as a duplicate.

That explanation cannot apply here:

  1. The variable IS present in the app's environment. On Linux the GUI session does export it — cinnamon-session carries it, and so does the desktop app. Verified on the direct child cowork-linux-helper (the main process's own /proc/<pid>/environ reads empty for unrelated reasons, which is itself an easy source of false negatives when diagnosing this):

``
pid 1057996 cowork-linux-helper ppid=<claude-desktop main> 83 env vars JIRA_API_TOKEN=present (192 chars)
``

  1. Both servers were spawned from that same environment, 41 s apart, and disagreed. A missing variable cannot produce a different result for two children of the same parent.

So there is a second defect, in the MCP spawn path itself, independent of the macOS environment-inheritance one.

Evidence

Reading ATLASSIAN_API_TOKEN straight from /proc/<pid>/environ of the spawned MCP servers, both with claude-desktop as ancestor:

pid 1058628  start 18:14:53  ancestor=DESKTOP  ATLASSIAN_API_TOKEN = <expanded, 192 chars>   OK
pid 1064857  start 18:15:34  ancestor=DESKTOP  ATLASSIAN_API_TOKEN = ${JIRA_API_TOKEN}       BROKEN

Same app instance (claude-desktop main started 18:14:40), same ~/.claude.json, 41 seconds apart.

For contrast, every MCP server spawned by the terminal CLI over the same day resolved correctly — 8 separate spawns between 08:58 and 18:00, all expanded(len=192).

The two desktop sessions were launched with different embedded-CLI argument shapes, which may be the discriminator:

  • resolved case — embedded CLI invoked with an inline --mcp-config {…} payload plus --strict-mcp-config
  • literal case — embedded CLI invoked with --strict-mcp-config and --setting-sources=user,project,local, no inline --mcp-config

Config

~/.claude.json (user scope), abridged:

"jira": {
  "command": "npx",
  "args": ["-y", "@aashari/mcp-server-atlassian-jira"],
  "env": {
    "ATLASSIAN_SITE_NAME": "<redacted>",
    "ATLASSIAN_USER_EMAIL": "<redacted>",
    "ATLASSIAN_API_TOKEN": "${JIRA_API_TOKEN}"
  },
  "type": "stdio"
}

JIRA_API_TOKEN is exported from a file symlinked into /etc/profile.d/, so every login shell — and, on this desktop environment, the graphical session too — has it.

What Should Happen?

${VAR} expansion should be deterministic: every MCP server spawned by the desktop app should resolve placeholders identically, using the app process's own environment. If a placeholder cannot be resolved, the spawn should fail loudly rather than pass the literal string through to the server, which turns a config error into a silent downgrade to unauthenticated access.

Reproduction

  1. Configure an MCP server in ~/.claude.json whose env references ${SOME_TOKEN}, with SOME_TOKEN exported by the graphical session (on Linux: a file in /etc/profile.d/).
  2. Launch the desktop app from the application menu.
  3. Open more than one Code-tab session in the same project, a few seconds apart.
  4. Inspect the spawned MCP servers: tr '\0' '\n' < /proc/<pid>/environ | grep TOKEN.

Expected: all spawned servers carry the resolved value. Actual: at least one carries the literal ${SOME_TOKEN}.

Note for anyone reproducing: read /proc/<pid>/environ for every matching pid, not just the first. The main process's environ can read back empty, which produces a false "the variable is missing" conclusion — that is how this was initially misdiagnosed here.

Workaround

Point command at a wrapper that sources the credentials file and resolves any inherited ${NAME}-shaped value before execing the real server. That makes the outcome identical on every surface, but it should not be necessary.

Environment

  • Claude Desktop: 1.24012.11 (Linux beta, apt)
  • Embedded CLI: 2.1.219 · standalone CLI: 2.1.221
  • Linux Mint 22.3 (Ubuntu 24.04 base), x86_64, X11, Cinnamon

View original on GitHub ↗