job7 step A: MAGIC_API_KEY by reference, not by value (BDR-026 follow-up)

toggle-external.sh's `claude mcp add magic --env API_KEY="$MAGIC_API_KEY"`
materialized the key as plaintext into ~/.claude.json — a copy outside the
~/.claude/.env canonical, invisible to the repo's gitignore/allowlist reach.
Claude Code supports ${VAR} expansion in mcpServers config (docs confirmed),
so the fix is a reference, not a scrub.

- lib/toggle-external.sh: --env 'API_KEY=${MAGIC_API_KEY}' (single-quoted
  literal reference, not bash-expanded) so future `enable magic` runs write
  the safe form too.
- README: new "Adding an MCP server that needs a secret" section documenting
  the --env pitfall and the wrapper pattern.

Out-of-repo companion changes (not in this commit): ~/.bashrc gained a
scoped claude() wrapper that sources ~/.claude/.env into a subshell before
exec'ing the real binary (verified: the var never reaches the ambient
interactive shell, only claude + children) — chosen over a global export to
keep the secret's surface minimal. ~/.claude.json's mcpServers.magic.env.API_KEY
was rewritten to the same "${MAGIC_API_KEY}" reference via a surgical jq
edit (never read directly, so the value never entered this session's
context). The 2 of 5 rotating ~/.claude/backups/.claude.json.backup.* files
still holding the old plaintext were scrubbed the same way.

Residual: this session predates the bashrc wrapper, so `claude mcp list`
currently warns "Missing environment variables: MAGIC_API_KEY" — expected,
resolves on next terminal + Claude Code restart. MAGIC_API_KEY rotation
still pending (user action, after this commit).
This commit is contained in:
Bastien Chanot
2026-07-07 12:37:29 +02:00
parent 3340c7d1bd
commit b9300c3382
3 changed files with 66 additions and 11 deletions
+36
View File
@@ -195,6 +195,42 @@ See [`templates/settings/SETTINGS.md`](templates/settings/SETTINGS.md) for the f
---
## Adding an MCP server that needs a secret
`claude mcp add <name> --env KEY=VALUE ...` writes `VALUE` **literally** into
`~/.claude.json` (or the project's `.mcp.json`) — if you pass the real secret
on that command line, it materializes as a second plaintext copy outside
`~/.claude/.env`, invisible to the repo's `.gitignore`/allowlist reach (this
bit us once: job7/BDR-026).
Claude Code expands `${VAR}` and `${VAR:-default}` in `mcpServers` config —
in `env`, `command`, `args`, `url`, and `headers` — for both project (`.mcp.json`)
and user (`~/.claude.json`) scope. Use that instead of a literal value:
```bash
# WRONG — plaintext key lands in ~/.claude.json:
claude mcp add magic --scope user --env API_KEY="$MAGIC_API_KEY" -- npx -y @21st-dev/magic@latest
# RIGHT — single-quoted so bash doesn't expand it; Claude Code expands it at
# launch, reading the var from its own process environment:
claude mcp add magic --scope user --env 'API_KEY=${MAGIC_API_KEY}' -- npx -y @21st-dev/magic@latest
```
The var still has to exist in the **environment of the process that starts
`claude`** — sourcing `~/.claude/.env` into your everyday interactive shell
would defeat the point (every subprocess, every stray `env`/`printenv`, would
then see it). This repo's `~/.bashrc` instead wraps the `claude` command
itself: a `claude()` shell function sources `~/.claude/.env` into a subshell
and `exec`s the real binary, so the var reaches `claude` and its children only
— never the ambient shell. See `lib/toggle-external.sh`'s `magic` case for
the pattern to copy for a new MCP server.
There is no `claude mcp add` flag that writes the reference form for you —
the `${VAR}` syntax has to be typed by hand (or via a wrapper script), same as
above.
---
## Diagnostic and maintenance
```bash