Build1 publisher2 min readPublished
Claude Code's .mcp.json expansion passes bare $VAR and unset ${VAR} to servers as raw text
Claude Code 2.1.278 left a bare $VAR unexpanded in all five .mcp.json fields a direct test probed, and sent an unset ${VAR} to the server as literal text. A shared config can hand a server a placeholder where a token belongs.
The Engineer · Build desk

What happened
- Claude Code's MCP docs, fetched on 2026-09-22, list two expansion forms, ${VAR} and ${VAR:-default}, applied across command, args, env, url and headers.
- Both documented forms expanded as described in every field the test covered, on stdio server entries and on an HTTP server entry.
- A nested default of the form ${A:-${B}} expanded only in headers, one of the two fields tested on the HTTP server entry.
- For an unset variable with no default, the docs say the config still loads and Claude Code shows a missing-variable warning in claude mcp list output.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure Any teammate whose shell lacks a referenced variable launches the shared server holding placeholder text where a credential or config value belongs.
- constraint A fallback chain such as ${A:-${B}} is unsafe to copy between fields on 2.1.278, since it resolves in headers and stays unresolved in the other four.
- decision Teams sharing one .mcp.json have a convention to settle: braces on every reference, an explicit :-default where a fallback beats a placeholder, and a per-machine check of claude mcp list.
The author of the dev.to post said that instead of guessing, they wrote a dependency-free stdio MCP server that reports its own process.argv and process.env, wired it into a .mcp.json several times over, and let Claude Code launch it [14]. The server answers initialize, ping, tools/list and tools/call over newline-delimited JSON-RPC and exposes one tool, echo_env [10]. At startup it appends a snapshot to spawn-log.jsonl, so a record of what the process received exists even when no model turn happens [11]. The run used a fresh mktemp -d directory, with no project instructions or memory files in play [12].
The config registers that one script seven times [13]. One entry carries all six forms in args and again as env keys. The other six differ only in command, because command is a single string and holds one form at a time [13]. Writing seven registrations is tedious, and it is the right call: each value is read back from the process itself, and the spawn log does not wait for a model to decide to call echo_env [11].
The docs list only the braced forms and do not mention $VAR without braces, or nesting [9]. In the test, an env key set to "$PROBE_SET" reached the server as the text $PROBE_SET [18][17]. The same happened in every other field, on the stdio entries and the HTTP one [4][1]. The server still launches, holding a variable's name where its value should be [17].
The unset case matched the docs [5][8]. The placeholder is a non-empty string. A server that only checks whether its token variable exists will find one, so the failure moves to whichever request first uses the token. I would not expect a rejected API call inside the server to send anyone to claude mcp list looking for that warning.
The author attributes the headers-only nesting result to headers being expanded twice [6]. On that account, the first pass would apply the outer default and leave ${PROBE_SET} in the string, and the second pass would resolve it [6].
Every result here comes from Claude Code 2.1.278 [2]. For these results to hold on another install, that install needs the same expander. The two braced forms, the five fields and the unset behavior are written into the docs [7][8]. The bare and nested results are observations of one release, and a later release could change either one without contradicting the page [9].
What to watch
- An update to the Claude Code MCP docs that covers bare $VAR or nested defaults, turning observed 2.1.278 behavior into stated behavior.
- A Claude Code release after 2.1.278 that expands nested defaults in all five fields, or in none, ending the headers-only split.
- Whether Claude Code starts surfacing the missing-variable warning at server launch as well as in claude mcp list.