From b9300c3382580684b55b3e865af9436e237ccbfd Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Tue, 7 Jul 2026 12:37:29 +0200 Subject: [PATCH] job7 step A: MAGIC_API_KEY by reference, not by value (BDR-026 follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .claude/tasks/TODO.md | 34 ++++++++++++++++++++++++---------- README.md | 36 ++++++++++++++++++++++++++++++++++++ lib/toggle-external.sh | 7 ++++++- 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 9eef3e8..6f046c1 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -17,16 +17,30 @@ manipuler une valeur de secret — edits sur les mécanismes seulement. Décision utilisateur : wiring `MAGIC_API_KEY` → wrapper `claude()` scopé dans `~/.bashrc` (source `.env` en subshell, jamais exporté globalement) plutôt qu'un export global (surface minimale, cohérent BDR-026). - - [ ] `~/.bashrc` : fonction `claude()` wrapper (subshell source ~/.claude/.env) - - [ ] `~/.claude.json` mcpServers.magic.env.API_KEY → `"${MAGIC_API_KEY}"` - (diff keys-only montré avant écriture) - - [ ] `lib/toggle-external.sh:191-192` — `--env API_KEY='${MAGIC_API_KEY}'` - (référence littérale, pas expansion bash) pour que les futurs - `enable magic` écrivent aussi la forme référence - - [ ] Doc README : procédure "ajouter un MCP avec secret" + piège `--env` - - [ ] Vérif manuelle : `claude mcp list` / relancer un MCP magic réel si possible -- [ ] A.3 Scrub one-shot des 5 backups `.claude.json.backup.*` existants - (prefix 78af0e36 hors `.env`) — script ou sed ciblé, vérif grep 0 hors .env + - [x] `~/.bashrc` : fonction `claude()` wrapper (subshell source ~/.claude/.env, + exec — vérifié : la var n'atteint QUE le subshell/exec, jamais le shell + parent). Hors repo (dotfile perso). + - [x] `~/.claude.json` mcpServers.magic.env.API_KEY → `"${MAGIC_API_KEY}"` + (diff keys-only montré avant écriture ; jq surgical edit, jamais Read + direct — la valeur n'a jamais traversé mon contexte). Backup fait + pendant l'édition supprimé aussitôt vérifié (aurait été un 6e leak). + - [x] `lib/toggle-external.sh:191-192` — `--env 'API_KEY=${MAGIC_API_KEY}'` + (référence littérale, single-quoted). `claude mcp add` direct au flag + bloqué par le classifieur auto-mode (self-modification non sollicitée, + respecté) — non testé live ; `claude mcp list` confirme la syntaxe + est bien reconnue ("Missing environment variables: MAGIC_API_KEY" — + attendu, cette session a démarré avant le wrapper bashrc). + - [x] Doc README : section "Adding an MCP server that needs a secret" + + piège `--env` + pattern wrapper à copier + - [x] Vérif manuelle : `claude mcp list` (read-only) — magic reconnaît + `${MAGIC_API_KEY}`, encore connecté (session pré-existante) ; nécessite + un restart terminal (source ~/.bashrc) + Claude Code pour confirmer + end-to-end — **résiduel, à faire par l'utilisateur** +- [x] A.3 Scrub backups `.claude.json.backup.*` — les 5 originaux (78af0e36 @ + job7 triage) déjà auto-rotés (ring-buffer natif) ; des 5 COURANTS, 2 + encore en clair (créés avant le fix, pendant cette session) → scrubbés + jq (mode 600 restauré, changé par erreur via mv). grep 78af0e36 : 0 hors + `.env` (backups + .claude.json confirmés propres). - [ ] A.4 Signaler à l'utilisateur : rotation MAGIC maintenant (après commit A) - [x] B. Redaction dumps d'env — `hooks/rtk-rewrite.sh` étendu : pipeline simple (pas de `;`/`&`/`||`) + `printenv`/`env` en tête sans `VAR=... cmd` derrière diff --git a/README.md b/README.md index 61970cb..a5f2b0e 100644 --- a/README.md +++ b/README.md @@ -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 --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 diff --git a/lib/toggle-external.sh b/lib/toggle-external.sh index 23bfefb..ca76e63 100755 --- a/lib/toggle-external.sh +++ b/lib/toggle-external.sh @@ -188,8 +188,13 @@ enable_tool() { warn "magic already enabled" return 0 fi + # Reference, not value: Claude Code expands ${VAR} in mcpServers.env at + # launch (job7/BDR-026) — MAGIC_API_KEY itself never lands in + # ~/.claude.json. The check above still confirms the var IS set in + # ~/.claude/.env before wiring the reference, so a missing key fails + # here instead of silently at Claude Code startup. claude mcp add magic --scope user \ - --env API_KEY="$MAGIC_API_KEY" \ + --env 'API_KEY=${MAGIC_API_KEY}' \ -- npx -y @21st-dev/magic@latest ok "magic enabled (user scope)" ;;