[BUG] Desktop: ${VAR} expansion for MCP env is non-deterministic within one app instance (Linux; not the macOS launchd-env cause of #70486)
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:
- The variable IS present in the app's environment. On Linux the GUI session does export it —
cinnamon-sessioncarries it, and so does the desktop app. Verified on the direct childcowork-linux-helper(the main process's own/proc/<pid>/environreads 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)
- 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-configand--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
- Configure an MCP server in
~/.claude.jsonwhoseenvreferences${SOME_TOKEN}, withSOME_TOKENexported by the graphical session (on Linux: a file in/etc/profile.d/). - Launch the desktop app from the application menu.
- Open more than one Code-tab session in the same project, a few seconds apart.
- 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