envHelper: settings-configurable command to supply env vars for config expansion (MCP configs, desktop app)
Problem
${VAR} expansion in MCP configs reads from Claude Code's own process environment. In the desktop app there is no shell in the launch path, so there is no good way to supply those variables, which pushes users toward pasting plaintext tokens into ~/.claude.json. The CLI has the same gap in a milder form: to feed env vars into MCP config expansion you have to wrap the whole session in an env-injecting launcher (e.g. varlock run -- claude, op run -- claude, doppler run -- claude), which is invocation-dependent and does not survive launching from an IDE or the desktop app.
headersHelper covers part of this, but only for HTTP/WS servers, only for headers, and per-server. It is also excluded from the MCP discovery cache (discovery-cache: headers-helper — purging this server's cache-key family, serving miss), so it re-runs at every session start. For helpers backed by a secrets manager that gates access on user presence (Touch ID / Windows Hello), that means an auth prompt on every session, including sessions that never use the server. Prompt fatigue then drives users back to plaintext tokens, which is the worst outcome.
Proposal
An envHelper setting (user and project scope), following the existing apiKeyHelper / awsCredentialExport pattern: a command that prints a JSON object of env var names to values.
// ~/.claude/settings.json or .claude/settings.json
{
"envHelper": "varlock load --format json"
}
Suggested semantics:
- Lazy: run the helper on the first
${VAR}expansion miss, not at startup. Combined with the MCP discovery cache, a session that never connects to a server needing these vars never runs the helper. - Merged into the expansion environment: applies wherever
${VAR}expansion already works (command,args,env,url,headers), including stdio serverenvblocks, so one helper serves N servers. Real environment variables take precedence; the helper fills misses. - Cached for the app process lifetime, with an optional TTL and re-run on MCP 401/403 retry as the refresh path (mirroring how
awsCredentialExportoutput is cached until expiry). This gives secret-manager-backed setups one consent prompt per app launch, only when actually needed, and Claude Code never persists the values. - Scope layering: user-level helper for personal secrets, project-level for project env, so
.mcp.jsonfiles with${VAR}references become invocation-independent across the CLI, desktop app, and IDE extensions.
Why not the existing mechanisms
- Plaintext
headers/envin config: the thing this is trying to avoid. headersHelper: HTTP-only, per-server, runs every session start due to the discovery-cache exclusion; no equivalent exists for stdio serverenv.- OAuth: right answer where servers implement it, but many internal/API-token servers never will.
- Wrapping the launch (
varlock run -- claude): works in a terminal, impossible from the desktop app, and couples the session to how it was launched.
For comparison, VS Code solves this custodially (MCP inputs with password: true prompt once and store in VS Code Secret Storage). An envHelper is the non-custodial version: the harness never stores a secret, it just knows what command to ask, and any secrets manager plugs in.