${VAR} / .env expansion for env values — keep secrets out of the config file #76
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Today every MCP server secret lives as plaintext in the config JSON. BCC masks it in the UI (
is_secret_key/_SecretMaskDelegate/redact_args), but the file on disk is still cleartext. Let an env value reference an environment variable instead:Previously declined (2026-07-02, in favour of masking alone); revived now.
The design question to settle first
Does BCC expand these, or does the client?
This determines whether the feature is real or a footgun. Claude Desktop and Claude Code do not, as far as we know, expand
${VAR}inmcpServersenv values — meaning if BCC writes${GITHUB_TOKEN}to disk and the client passes it through literally, the server receives the literal string${GITHUB_TOKEN}and fails at runtime with a confusing auth error.So the first task is verifying actual client behaviour, not writing the expander. Depending on the answer:
Do not build the expander before this is answered.
Scope once decided
expand_env_refs(value, environ)inbcc_core—${VAR}, unset-variable handling, escaping for a literal$.${VAR}placeholder is not a secret and should render in the clear — masking it would defeat the point.args_secret_warning: a placeholder in args should suppress the warning rather than trigger it.Related
Pairs with the previously-declined OS keychain integration; if
${VAR}lands well, keychain becomes the natural follow-on.Blocker resolved — and the answer splits by client
I said not to build the expander until we knew whether the client does this itself. Checked Anthropic's current docs. Claude Code expands
${VAR}natively. Claude Desktop has no documented support. That changes the design substantially: this isn't "BCC expands variables", it's "BCC authors a client feature, and must know which clients have it."Claude Code — yes, fully
Documented syntax:
${VAR}— expands to the environment variable${VAR:-default}— expands toVARif set, otherwisedefaultExpanded in all five locations we care about:
command,args,env,url,headers.Their own example:
Scope isn't limited to
.mcp.json— the docs also describe${VAR}expansion in "a local- or user-scoped server entry in~/.claude.json", which is exactly the file BCC edits for the Claude Code profile.Unset-variable behaviour is specified, which spares us inventing one: the config still loads, Claude Code reports a missing-variable warning in
claude mcp list, and passes the unexpanded${VAR}text through as-is.Claude Desktop — no
Nothing in Anthropic's documentation describes expansion for
claude_desktop_config.json; the feature is documented as a Claude Code capability throughout. Field reports also suggest Desktop'senvhandling is more limited in general. So a${VAR}written into a Desktop config is a literal string handed to the server — the confusing-auth-failure footgun I flagged when filing this.I'm treating "undocumented" as "don't rely on it" rather than proving a negative.
What this means for the design
The good news: BCC already knows which client a profile targets —
profile_targets_claude_desktop()exists for the restart button. So the feature can be gated per-profile rather than being globally right or globally dangerous:~/.claude.json, project.mcp.json) — placeholders are a first-class supported feature. Support${VAR:-default}too, not just${VAR}; it's half the value and it's what makes shared configs work.BCC should not expand at write time. Writing the resolved secret to disk is what the user is trying to avoid, and on Claude Code it would defeat a feature the client already implements correctly. BCC's job is authoring, validating, and warning.
Two interactions to get right, both currently backwards for this feature:
is_secret_key("API_KEY")is true, so_SecretMaskDelegatewould mask${API_KEY}into dots — making a reference indistinguishable from a real stored secret, which is precisely the distinction the user needs to see.args_secret_warningshould be silenced by a placeholder, not triggered. Moving a token into${VAR}is the recommended fix for that warning; still warning afterwards punishes the fix.Unblocked — implementing against these semantics.
Sources: Connect Claude Code to tools via MCP