diff --git a/.audit/scan-secrets-claude-home.json b/.audit/scan-secrets-claude-home.json index 27d2c81..869424d 100644 --- a/.audit/scan-secrets-claude-home.json +++ b/.audit/scan-secrets-claude-home.json @@ -59,46 +59,6 @@ "Tags": [], "Fingerprint": "/home/bchanot/.claude/ide/20429.lock:generic-api-key:1" }, - { - "RuleID": "sourcegraph-access-token", - "Description": "Sourcegraph is a code search and navigation engine.", - "StartLine": 579, - "EndLine": 579, - "StartColumn": 17, - "EndColumn": 57, - "Match": "REDACTED\"", - "Secret": "REDACTED", - "File": "/home/bchanot/.claude/paste-cache/7d48f52c7499c1a7.txt", - "SymlinkFile": "", - "Commit": "", - "Entropy": 3.6628149, - "Author": "", - "Email": "", - "Date": "", - "Message": "", - "Tags": [], - "Fingerprint": "/home/bchanot/.claude/paste-cache/7d48f52c7499c1a7.txt:sourcegraph-access-token:579" - }, - { - "RuleID": "sourcegraph-access-token", - "Description": "Sourcegraph is a code search and navigation engine.", - "StartLine": 590, - "EndLine": 590, - "StartColumn": 17, - "EndColumn": 57, - "Match": "REDACTED\"", - "Secret": "REDACTED", - "File": "/home/bchanot/.claude/paste-cache/7d48f52c7499c1a7.txt", - "SymlinkFile": "", - "Commit": "", - "Entropy": 3.7275672, - "Author": "", - "Email": "", - "Date": "", - "Message": "", - "Tags": [], - "Fingerprint": "/home/bchanot/.claude/paste-cache/7d48f52c7499c1a7.txt:sourcegraph-access-token:590" - }, { "RuleID": "github-pat", "Description": "Uncovered a GitHub Personal Access Token, potentially leading to unauthorized repository access and sensitive content exposure.", @@ -119,26 +79,6 @@ "Tags": [], "Fingerprint": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/27758e8c-36b5-4816-8141-8b07ba28b0c8.jsonl:github-pat:194" }, - { - "RuleID": "generic-api-key", - "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", - "StartLine": 10, - "EndLine": 10, - "StartColumn": 676, - "EndColumn": 730, - "Match": "nGITEA_TOKEN=REDACTED\\n", - "Secret": "REDACTED", - "File": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/960bd2cf-7915-479e-a9d7-616a463789f9.jsonl", - "SymlinkFile": "", - "Commit": "", - "Entropy": 3.7282128, - "Author": "", - "Email": "", - "Date": "", - "Message": "", - "Tags": [], - "Fingerprint": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/960bd2cf-7915-479e-a9d7-616a463789f9.jsonl:generic-api-key:10" - }, { "RuleID": "jwt", "Description": "Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.", @@ -159,46 +99,6 @@ "Tags": [], "Fingerprint": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/d71e6b88-7632-40e9-b7bc-830fb32fc464/tool-results/bsl3i4eop.txt:jwt:164" }, - { - "RuleID": "aws-access-token", - "Description": "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.", - "StartLine": 652, - "EndLine": 652, - "StartColumn": 275, - "EndColumn": 294, - "Match": "REDACTED", - "Secret": "REDACTED", - "File": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/4b5c02a9-3acd-4941-951e-134a569afe02.jsonl", - "SymlinkFile": "", - "Commit": "", - "Entropy": 3.5464394, - "Author": "", - "Email": "", - "Date": "", - "Message": "", - "Tags": [], - "Fingerprint": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/4b5c02a9-3acd-4941-951e-134a569afe02.jsonl:aws-access-token:652" - }, - { - "RuleID": "aws-access-token", - "Description": "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.", - "StartLine": 652, - "EndLine": 652, - "StartColumn": 671, - "EndColumn": 690, - "Match": "REDACTED", - "Secret": "REDACTED", - "File": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/4b5c02a9-3acd-4941-951e-134a569afe02.jsonl", - "SymlinkFile": "", - "Commit": "", - "Entropy": 3.5464394, - "Author": "", - "Email": "", - "Date": "", - "Message": "", - "Tags": [], - "Fingerprint": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/4b5c02a9-3acd-4941-951e-134a569afe02.jsonl:aws-access-token:652" - }, { "RuleID": "generic-api-key", "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", @@ -325,6 +225,46 @@ ], "Fingerprint": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/f1c9c474-84b6-4484-b53f-25aad840e8fd.jsonl:aws-access-token:52" }, + { + "RuleID": "aws-access-token", + "Description": "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.", + "StartLine": 652, + "EndLine": 652, + "StartColumn": 275, + "EndColumn": 294, + "Match": "REDACTED", + "Secret": "REDACTED", + "File": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/4b5c02a9-3acd-4941-951e-134a569afe02.jsonl", + "SymlinkFile": "", + "Commit": "", + "Entropy": 3.5464394, + "Author": "", + "Email": "", + "Date": "", + "Message": "", + "Tags": [], + "Fingerprint": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/4b5c02a9-3acd-4941-951e-134a569afe02.jsonl:aws-access-token:652" + }, + { + "RuleID": "aws-access-token", + "Description": "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.", + "StartLine": 652, + "EndLine": 652, + "StartColumn": 671, + "EndColumn": 690, + "Match": "REDACTED", + "Secret": "REDACTED", + "File": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/4b5c02a9-3acd-4941-951e-134a569afe02.jsonl", + "SymlinkFile": "", + "Commit": "", + "Entropy": 3.5464394, + "Author": "", + "Email": "", + "Date": "", + "Message": "", + "Tags": [], + "Fingerprint": "/home/bchanot/.claude/projects/-home-bchanot-Documents-claude/4b5c02a9-3acd-4941-951e-134a569afe02.jsonl:aws-access-token:652" + }, { "RuleID": "generic-api-key", "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index fa18278..f484413 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -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. diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index 547a725..c15a52b 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -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]]. diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 6b5b530..709b3fd 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -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).