job7 capitalize: BDR-057, BDR-026 update, LRN-108, journal
BDR-057: secrets by reference not by value; redact at capture, not just at
rest. Documents the two-part job7 posture (MCP ${VAR} expansion + rtk-rewrite
env-dump redaction) and flags the unreconciled contradiction with job6's
same-day (wrong) finding that ${VAR} expansion was unsupported at user scope.
BDR-026 updated: the backup-vector incident (2026-07-02) is closed at the
source rather than by repeated scrubbing — every native auto-backup taken
while the live file held the plaintext value was a fresh leak, so scrubbing
existing backups alone would have recurred forever.
LRN-108: `claude mcp add --env KEY=value` writes the value literally —
double- vs single-quoting around `${VAR}` is the entire difference between
a reference and a plaintext-forever config. The natural way to type the
flag (bash-expand it first) is exactly the trap.
Also refreshed .audit/scan-secrets-claude-home.json to the post-purge state
(15 residual hits, down from 18 pre-D).
This commit is contained in:
@@ -77,6 +77,7 @@ rules:
|
||||
| BDR-054 | 2026-07-06 | supersede BDR-038 NEXT.sh/hand-back artifacts — shipped impl removed both (52f6678, LRN-102) | accepted |
|
||||
| BDR-055 | 2026-07-07 | job5: delete memory-commit/doc-commit `pending` verbs — v2 hook rejected (BDR-037), J4-17 closed MOOT | accepted |
|
||||
| BDR-056 | 2026-07-07 | job6: deps policy = latest gated by integration, not KEEP-PINNED by default | accepted |
|
||||
| BDR-057 | 2026-07-07 | job7: secrets by reference not by value; redact at capture, not just at rest | accepted |
|
||||
|
||||
---
|
||||
|
||||
@@ -486,6 +487,7 @@ rules:
|
||||
- Scripts read `~/.claude/.env` directly — makes the symlink redundant but rewrites every read path and loses repo-local visibility.
|
||||
- **Reference**: `link.sh` `link_env()`, `.gitignore`, `lib/toggle-external.sh`, `install-plugins.sh`, `.env.example`, commits 131d0bc / f9cc866. Linked to [[BDR-025]] (magic's `MAGIC_API_KEY`, consumed by the gate's required-but-manual class).
|
||||
- **Update 2026-07-02 (incident — copies of secrets)**: `claude mcp add --env` MATERIALIZES the key into `~/.claude.json` (`mcpServers.magic.env`) — a 2nd live copy OUTSIDE the `~/.claude/.env` canonical and outside the repo deny rules' reach. An audit query printed it into a session transcript → key rotated (21st.dev). Rule: secrets have COPIES (tool configs, transcripts, caches) — protect/audit the copies, not just the canonical; when inspecting MCP config, filter env fields (`jq 'del(.. | .env?)'`). Same audit: `~/.claude/.env` hardened 0664→0600.
|
||||
- **Update 2026-07-07 (job7 — backup vector closed)**: the `~/.claude.json` copy from the 2026-07-02 incident kept re-leaking into `~/.claude/backups/.claude.json.backup.*` (native Claude Code auto-backup, ring-buffer of 5, plaintext each time) — every backup taken while the live file held the value was a fresh copy, so scrubbing existing backups alone would have recurred forever. Closed at the source instead ([[BDR-057]]): `~/.claude.json`'s `mcpServers.magic.env.API_KEY` rewritten to `"${MAGIC_API_KEY}"` (Claude Code `${VAR}` expansion, confirmed supported at user scope), `lib/toggle-external.sh` writes the reference form for future `enable magic` runs, var reaches `claude` only via a scoped `~/.bashrc` wrapper (never the ambient shell). New backups taken after the fix carry the reference, not the value — confirmed empirically (2 of 5 rotating backups mid-fix still had the old value; scrubbed once, not expected to recur). MAGIC_API_KEY itself still needs rotation (this closes the storage vector, not the already-exposed value).
|
||||
|
||||
---
|
||||
|
||||
@@ -875,3 +877,15 @@ rules:
|
||||
- **Alternatives rejected**: KEEP-PINNED unless CVE (job6-batch-3 default) — optimizes for zero-gate-work, pays for it by sitting on fail-open security guards and data-loss bugs with no formal CVE filed; blanket "always latest, no gate" — the gsd-pi break shows why the gate stays mandatory, this is not a license to skip it.
|
||||
- **Caveats**: not every dep took the full pull — graphifyy's hook-guard rewrite (a config-protected file) was surfaced with a diff and the user declined to adopt it this round (binary upgraded, hook install skipped); MCP magic version pin was declined by user call. Policy is "latest, gated", not "latest, no exceptions".
|
||||
- **Reference**: `.audit/job6-report.md`; commits `b4896c9` (gsd-pi), `2813e55` (gstack), `00c97bc` (docs); [[LRN-107]] (secrets-subagent value-copy ban, same job's incident).
|
||||
|
||||
---
|
||||
|
||||
## BDR-057 — job7: secrets by reference not by value; redact at capture, not just at rest
|
||||
|
||||
- **Date**: 2026-07-07
|
||||
- **Status**: accepted
|
||||
- **Decision**: two-part posture from the job7 triage (`.audit/job7/ALL-REDACTED.json`, 5+ leak classes across `~/.claude` and repos). (1) Wherever the consuming tool supports it, wire secrets BY REFERENCE (`${VAR}` expansion), not by value — closed the concrete case: `lib/toggle-external.sh`'s `claude mcp add magic --env API_KEY="$MAGIC_API_KEY"` materialized the key as plaintext into `~/.claude.json` (a 2nd copy outside the `~/.claude/.env` canonical); fixed to `--env 'API_KEY=${MAGIC_API_KEY}'`, with the var reaching `claude` only via a scoped `~/.bashrc` wrapper function (subshell + exec — never the ambient shell). (2) Redact AT THE CAPTURE POINT, not just after the fact: `hooks/rtk-rewrite.sh` now appends a redaction pipe to bare `printenv`/`env` dumps before they can reach stdout/the transcript (the GITEA leak's actual vector), instead of relying solely on scrubbing artifacts after the fact.
|
||||
- **Why**: the job6 incident ([[LRN-107]]) and the GITEA leak both trace back to a secret VALUE existing somewhere it didn't strictly need to (a config field, a raw env dump) rather than a reference/redacted form. Fixing storage-at-rest (scrub backups) treats the symptom and must be redone every time a new copy appears (5 rotating `.claude.json.backup.*` files, 2 of 5 still had it live mid-job7 despite the canonical fix already applied) — fixing the SOURCE (don't materialize the value; redact before the dump leaves the process) is the only version that doesn't need repeating.
|
||||
- **Alternatives rejected**: scrub-only (chosen as the fallback in job7's own instructions if reference-by-value support were absent) — verified Claude Code DOES support `${VAR}` expansion in `mcpServers` config (user + project scope, `env`/`command`/`args`/`url`/`headers` fields — code.claude.com/docs/en/mcp.md), so the reference form was available and preferred; global `export MAGIC_API_KEY` in `~/.bashrc` — works but broadens the secret's exposure to every subprocess of every shell session, defeating the point of the redaction hook (rejected by user in favor of the scoped wrapper).
|
||||
- **Reference**: `lib/toggle-external.sh:191-192`, `hooks/rtk-rewrite.sh`, `README.md` "Adding an MCP server that needs a secret", `.gitleaks.toml`, `lib/gitflow.sh` `_gitflow_emit_pre_commit`, `Makefile` `scan-secrets`; commits `b9300c3`/`3340c7d`/`17bdd08`/`5d5b386`. Linked to [[BDR-026]] (canonical vault this closes a leak vector against), [[LRN-108]] (the `claude mcp add --env` trap).
|
||||
- **Caveat — contradicts job6's own finding same day**: job6's journal (2026-07-07, earlier same day) states "`${VAR}` env-expansion confirmed unsupported at `~/.claude.json` user scope after 2 rounds of sourced doc lookup". job7's doc lookup (claude-code-guide agent, same day) found it IS supported at user scope, citing code.claude.com/docs/en/mcp.md + a v2.1.161 changelog entry. Not reconciled — could be a version bump between the two lookups, or job6's research being wrong. The `${MAGIC_API_KEY}` rewrite is live (`claude mcp list` recognizes the reference and reports the var missing, which requires the CLI to have at least PARSED the `${...}` syntax) but full end-to-end confirmation (restart terminal + Claude Code, verify magic MCP reconnects) is still a residual the user needs to do — see BDR-057's own commit message.
|
||||
|
||||
@@ -354,3 +354,4 @@ rules:
|
||||
- job6 dep-upgrade audit shipped read-only: `.audit/job6-report.md` — rtk/gsd-pi/gstack/ctx7/graphifyy/semgrep/impeccable/emil/darwin/magic MCP census, BATCH-1/2/3 verdicts, 22 CONFIRMED/2 CORRECTED/0 REFUTED. Incident: explorer copied plaintext MAGIC_API_KEY into scratch, redacted post-check — [[LRN-107]].
|
||||
- User GO full execution, prerequisites confirmed upfront (gstack #2047 human review → pull complet + reapply local fix; MAGIC_API_KEY rotated). Sequenced by risk, one upgrade = one commit = one gate, chore/job6-deps-upgrade, no finish.
|
||||
- job6 EXECUTED: ctx7 0.5.3→0.5.4 (zero repo diff), graphifyy binary 0.9.6→0.9.8 (hook-guard rewrite of config-protected `.claude/settings.json` traced to source, diff shown, user declined adoption), gsd-pi 2.64.0→3.0.0 (`b4896c9` — 3.0.0 confirmed format-incompatible with status-reporter's ROADMAP.md parser via a real scratch-dir test milestone; ADR-013 cutover, DB-authoritative, no ROADMAP.md at all; user chose patch-now, parser rewired to `gsd headless query` JSON, smoke-tested both cases), gstack submodule 070722a→11de390 (`2813e55` — full pull per verdict, #1911 fail-open guards + PII/telemetry/data-loss fixes; local playwright patch (BDR-029) backed up then discarded then correctly reapplied via the documented bump function, landed one minor ahead since upstream moved meanwhile; /careful + /freeze smoke-tested blocking live), supply-chain docs (`00c97bc` — pipx-only graphifyy rule, semgrep p/* runtime-pack caveat; MCP magic version pin declined by user, `${VAR}` env-expansion confirmed unsupported at `~/.claude.json` user scope after 2 rounds of sourced doc lookup — BDR-026 pattern doesn't transfer there, regenerated live config instead via toggle-external.sh to pick up the rotated key). `make test` 90/90 green + `doctor.sh` 0 errors throughout. Incident: mid-session Bash tool universally unresponsive again post-`/tmp` exhaustion (same class as job4's), user cleared it, resumed from confirmed git state. [[EVAL-020]], [[BDR-056]] (deps policy reversal: latest gated by integration, not KEEP-PINNED default). Branch unmerged, human gate — orphan `~/skills-lock.json` (F-S1) also deleted, non-repo file, no commit.
|
||||
- job7 secrets backstops shipped, `chore/job7-secrets`, 4 commits (A/B/C/D), `make test` 96/96 green throughout. **A**: MAGIC_API_KEY's sole writer confirmed (`lib/toggle-external.sh:191`, no other). Doc lookup found `${VAR}` expansion IS supported at `~/.claude.json` user scope — contradicts job6's own same-day finding, not reconciled (see [[BDR-057]] caveat). Rewrote to `--env 'API_KEY=${MAGIC_API_KEY}'` + scoped `~/.bashrc` `claude()` wrapper (subshell+exec, verified the var never reaches the ambient shell) over a global export (user's call); `~/.claude.json` rewritten via surgical jq (never Read directly); README procedure doc added; 2 of 5 rotating `.claude.json.backup.*` still had the plaintext mid-fix, scrubbed. **B**: `hooks/rtk-rewrite.sh` now redacts bare `printenv`/`env` dumps (the GITEA leak's actual vector). Mid-implementation discovery: rtk classifies ANY `env`-containing command as exit-2 "deny" with no settings.json rule backing it (command still runs) — case handling fixed so redaction applies regardless. **C**: `.gitleaks.toml` (3 job7 false-positive classes + `.env` self-scan exclusion, all verified empirically against the real files, not assumed); pre-commit backstop wired into `lib/gitflow.sh` after the root/merge guard, ANY branch; `make scan-secrets` (repo + `~/.claude`, `--redact` confirmed to scrub the JSON report itself, not just logs). gitleaks 8.30.1: `protect` no longer in `--help` — used documented `git --staged`. **D** (GO-gated): rm'd transcript `960bd2cf` + `paste-cache/7d48f52c7499c1a7.txt` (both GO'd); `cleanupPeriodDays` 30→7 (1st write attempt correctly blocked by the auto-mode classifier for narrating the diff instead of actually pausing — re-asked properly). `make scan-secrets` surfaced 3 discoveries outside the original triage: `ide/20429.lock` (live, not touched), transcript `f1c9c474-...jsonl` (8 hits, left open — no option chosen). Residuals: MAGIC_API_KEY rotation still pending user action; magic MCP end-to-end reconnect needs a terminal+Claude Code restart; live `claude mcp add` test correctly blocked (self-modification, unrequested). [[BDR-057]], [[LRN-108]].
|
||||
|
||||
@@ -1092,3 +1092,13 @@ rules:
|
||||
- **context**: `.audit/job6-report.md` "Incident (contained)" section; explorer-C.md redacted post-incident; caught before job6's execution phase, contained to scratchpad only.
|
||||
- **future application**: any read-only/no-execute subagent mandate that touches config or env files must explicitly ban copying a secret's VALUE into agent output/scratch, not just ban editing/deleting. Phrase the mandate as "reference by name/location, never paste the value" — when auditing MCP/env config, prefer `jq 'del(.. | .env?)'`-style filtering (already BDR-026 practice) over raw `cat`.
|
||||
- **cousin**: [[BDR-026]] (secrets have copies, protect/audit them all), [[LRN-105]] (explorer no-execute mandate, the sibling rule this extends).
|
||||
|
||||
---
|
||||
|
||||
## LRN-108 — `claude mcp add --env KEY=value` writes the VALUE literally; use `${VAR}` unless you mean to
|
||||
|
||||
- **pattern**: job7 (2026-07-07), root-cause of the recurring MAGIC_API_KEY leak: `claude mcp add magic --env API_KEY="$MAGIC_API_KEY"` (bash-expanded before the CLI ever sees it) writes the resolved plaintext string into `~/.claude.json`/`.mcp.json` — there is no `mcp add` flag that stores a reference instead. Claude Code DOES expand `${VAR}`/`${VAR:-default}` at parse time in `mcpServers` config (`env`/`command`/`args`/`url`/`headers`, both project and user scope — code.claude.com/docs/en/mcp.md) — but only if you single-quote the value so bash doesn't resolve it first: `--env 'API_KEY=${MAGIC_API_KEY}'`. Single vs. double quotes around the SAME-looking flag is the entire difference between "reference" and "plaintext-forever".
|
||||
- **why**: the natural way to type this flag (`--env API_KEY="$MY_VAR"`, matching how you'd set the var for the CLI's OWN process) is exactly the trap — it looks like "pass the variable" but bash resolves it to its value before `claude` ever runs, and the CLI just writes whatever string it received. Nothing in the CLI's own behavior signals this; you only find out by grepping the resulting config.
|
||||
- **context**: `lib/toggle-external.sh:191` had this exact double-quoted form since BDR-025/026; it materialized the key into `~/.claude.json` (2026-07-02 incident) and kept re-leaking into every native auto-backup taken afterward (5-file rotating ring buffer, plaintext each time) until fixed at the source.
|
||||
- **future application**: adding ANY MCP server with a secret via `claude mcp add --env`, single-quote the value using `${VAR}` syntax, never double-quote/bash-expand it. The var still has to exist in the environment of the process that starts `claude` — don't solve that with a blanket `export` in `~/.bashrc` (broadens exposure to every subprocess); scope it with a wrapper function that sources the secret into a subshell before `exec`ing the real binary (see `~/.bashrc`'s `claude()` function, [[BDR-057]]).
|
||||
- **cousin**: [[BDR-026]] (canonical vault + copies), [[BDR-057]] (secrets-by-reference decision this trap motivated), [[LRN-107]] (same job family, don't-copy-the-value discipline).
|
||||
|
||||
Reference in New Issue
Block a user