${VAR} / .env expansion for env values — keep secrets out of the config file #76

Open
opened 2026-07-20 12:07:41 -04:00 by the_og · 1 comment
Owner

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:

"env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }

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} in mcpServers env 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:

  • Client expands it → BCC writes the placeholder through and just needs display/validation support. Clean win.
  • Client does not → BCC must expand at write time (config on disk holds the real value; the placeholder is only an authoring convenience), which gives a much weaker security story and should be presented honestly as such. Alternatively BCC pins the resolved value and warns when the referenced variable is unset.

Do not build the expander before this is answered.

Scope once decided

  • Pure expand_env_refs(value, environ) in bcc_core${VAR}, unset-variable handling, escaping for a literal $.
  • Interaction with secret masking: a ${VAR} placeholder is not a secret and should render in the clear — masking it would defeat the point.
  • Interaction with args_secret_warning: a placeholder in args should suppress the warning rather than trigger it.
  • Warn on save when a referenced variable is unset in BCC's own environment (best-effort — the client's environment may differ, and that caveat should be in the warning text).

Related

Pairs with the previously-declined OS keychain integration; if ${VAR} lands well, keychain becomes the natural follow-on.

## 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: ```json "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } ``` 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}` in `mcpServers` env 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: - **Client expands it** → BCC writes the placeholder through and just needs display/validation support. Clean win. - **Client does not** → BCC must expand at *write* time (config on disk holds the real value; the placeholder is only an authoring convenience), which gives a much weaker security story and should be presented honestly as such. Alternatively BCC pins the resolved value and warns when the referenced variable is unset. Do not build the expander before this is answered. ## Scope once decided - Pure `expand_env_refs(value, environ)` in `bcc_core` — `${VAR}`, unset-variable handling, escaping for a literal `$`. - Interaction with secret masking: a `${VAR}` placeholder is **not** a secret and should render in the clear — masking it would defeat the point. - Interaction with `args_secret_warning`: a placeholder in args should suppress the warning rather than trigger it. - Warn on save when a referenced variable is unset in BCC's own environment (best-effort — the client's environment may differ, and that caveat should be in the warning text). ## Related Pairs with the previously-declined OS keychain integration; if `${VAR}` lands well, keychain becomes the natural follow-on.
the_og added the P2 label 2026-07-20 12:07:41 -04:00
Author
Owner

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 to VAR if set, otherwise default

Expanded in all five locations we care about: command, args, env, url, headers.

Their own example:

{
  "mcpServers": {
    "api-server": {
      "type": "http",
      "url": "${API_BASE_URL:-https://api.example.com}/mcp",
      "headers": { "Authorization": "Bearer ${API_KEY}" }
    }
  }
}

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's env handling 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 targetsprofile_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 Code profiles (~/.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.
  • Claude Desktop profiles — warn on save that placeholders will reach the server literally.

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:

  1. Secret masking must skip placeholders. is_secret_key("API_KEY") is true, so _SecretMaskDelegate would mask ${API_KEY} into dots — making a reference indistinguishable from a real stored secret, which is precisely the distinction the user needs to see.
  2. args_secret_warning should 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

## 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 to `VAR` if set, otherwise `default` Expanded in **all five** locations we care about: `command`, `args`, `env`, `url`, `headers`. Their own example: ```json { "mcpServers": { "api-server": { "type": "http", "url": "${API_BASE_URL:-https://api.example.com}/mcp", "headers": { "Authorization": "Bearer ${API_KEY}" } } } } ``` 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's `env` handling 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 Code profiles** (`~/.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. - **Claude Desktop profiles** — warn on save that placeholders will reach the server literally. 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: 1. **Secret masking must skip placeholders.** `is_secret_key("API_KEY")` is true, so `_SecretMaskDelegate` would mask `${API_KEY}` into dots — making a reference indistinguishable from a real stored secret, which is precisely the distinction the user needs to see. 2. **`args_secret_warning` should 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](https://docs.claude.com/en/docs/claude-code/mcp)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: the_og/better-claude-config#76