[BUG] MCP loader env-var validator misparses POSIX parameter expansion (e.g. ${var%pattern}) as missing env var
Summary
Claude Code's MCP-config validator scans the args[] strings of stdio MCP servers for ${...} placeholders and treats anything inside the braces as an environment variable name. The matching regex appears to be greedy and accepts any character between ${ and }, so POSIX shell parameter-expansion forms like ${var%pattern} (suffix removal), ${var#prefix} (prefix removal), ${var/old/new} (substitution), and so on are misinterpreted as env-var references. The validator then refuses to start the server, complaining that no env var with that name exists.
Real environment-variable identifiers must match [A-Za-z_][A-Za-z0-9_]*. Names like _R%/ cannot exist — % and / are not legal in identifiers. So the validator rejecting them as "missing" is a parser bug, not a config error.
Minimal repro
A .mcp.json whose stdio server uses any POSIX parameter expansion in args[]:
{
"mcpServers": {
"demo": {
"type": "stdio",
"command": "sh",
"args": ["-c", "_X=foo; echo \"${_X%o}\""]
}
}
}
Expected: server starts; sh evaluates ${_X%o} as suffix-strip, prints fo.
Actual: validator refuses to start the server with Missing environment variables: _X%o.
In the wild
This is currently breaking the claude-mem plugin (claude-mem@thedotmack v12.7.5+), whose .mcp.json ships a shell-script auto-locator that uses ${_R%/} to strip a trailing slash from a path:
... | while IFS= read -r _R; do _R="${_R%/}"; ...
Symptom: Plugin (claude-mem @ claude-mem@thedotmack): MCP server mcp-search invalid: Missing environment variables: _R%/. This breaks every plugin tool that depends on the MCP server (memory search, knowledge graph queries, timeline, etc.).
Reports on the plugin tracker confirming the same root cause:
- thedotmack/claude-mem#2356 (most detailed, includes suggested upstream patch)
- thedotmack/claude-mem#2354
- thedotmack/claude-mem#2350
The plugin author has a legitimate reason to use POSIX expansion: per thedotmack/claude-mem#2347, Claude Code substitutes ${CLAUDE_PLUGIN_ROOT} into args[] but does not export it as an env var, so a sh -c wrapper can't see it — they need an in-script auto-locator that finds the install dir from filesystem heuristics. POSIX parameter expansion is the natural tool for that.
Suggested fix
Tighten the env-var detection regex from "anything inside ${...}" to a proper env-var identifier with optional default-value suffix:
\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}
This still matches the documented forms Claude Code intends to support (${VAR}, ${VAR:-default}) and now correctly skips POSIX expansions like ${var%pattern}, ${var#prefix}, ${var/old/new}, ${#var}, etc. so they reach the spawned shell intact.
If parameter expansion needs to be expressly forbidden (e.g. for sandbox reasons), the validator should produce a clearer error — something like "args[N] contains POSIX parameter expansion ${_R%/}; only ${VAR} and ${VAR:-default} are supported in MCP args" — instead of pretending an unrelated env var is missing.
Environment
- OS: Linux x86_64 (same root cause reported on macOS and Windows in the plugin issues above)
- Claude Code version: latest stable as of 2026-05-08
- MCP host: stdio via
.mcp.json
Workaround for plugin authors targeting Claude Code only
Avoid sh -c wrappers in args[]; use command: "node" (or the language runtime directly) plus a direct file path with ${CLAUDE_PLUGIN_ROOT} substitution. Example:
{
"mcpServers": {
"my-server": {
"type": "stdio",
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/mcp-server.cjs"]
}
}
}
This skips the shell entirely so parameter expansion never enters the picture. The downside is loss of in-script logic for handling multi-host install layouts (Codex CLI, secondary CLAUDE_CONFIG_DIR, marketplace dev mode), which is the very capability claude-mem was trying to preserve.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗