[security] Built-in secret redaction at jsonl write-time in ~/.claude/projects/

Resolved 💬 2 comments Opened May 29, 2026 by ibossyNr1 Closed Jun 1, 2026

Summary

Claude Code persists every sub-agent transcript to ~/.claude/projects/<id>/*.jsonl and records tool stdout verbatim. If a shell command echoes a secret (echo \$API_KEY, gcloud iam service-accounts keys create out.json, cat creds.env, env-var dumps from printenv, etc.), the secret is now durably on disk in the session transcript with no built-in redaction.

This becomes a leak vector when:

  • the transcript dir is checked into git (a config-sync repo or dotfile mirror)
  • backups (Time Machine, Windows File History) sweep up the transcripts
  • screen recording / screen sharing surfaces the transcript pane
  • a future tool reads the transcript for context and emits the secret into a new artifact

Incident that motivated this

We hit this in production. 15 GitHub secret-scanning alerts on a private dotfile-sync repo (~/.claude/ mirrored to git). Eight distinct secret families surfaced inside sub-agent jsonl files:

| Family | Pattern |
|---|---|
| Anthropic API key | sk-ant-api03-… |
| OpenRouter API key | sk-or-v1-<64 hex> |
| Stripe webhook signing secret | whsec_… |
| Supabase Personal Access Token | sbp_<40 hex> |
| Telegram Bot Token | <bot_id>:<35+ char alpha> (one alert was \publicly_leaked=true\) |
| GCP Service Account JSON (full key) | \"type\":\"service_account\" … \"private_key\":\"-----BEGIN PRIVATE KEY-----…\" |
| GitHub fine-grained PAT | github_pat_… |
| GitHub OAuth access token | gho_… |

Root cause: the transcript writer doesn't run any redaction pass. We had to ship a SessionEnd hook + a pre-commit gitleaks rule set + git history rewrite to remediate. None of these layers would have been necessary if the transcript writer redacted at write time.

Proposed feature

Write-time secret redaction in the jsonl persistence layer.

  • Opt-in via a Claude Code setting (e.g. \"transcripts.redactSecrets\": true\).
  • Rule set: the same families above, extensible via a user-provided ~/.claude/secret-rules.toml\ (compatible with the gitleaks format would be ideal).
  • Replacement marker: \[REDACTED:<rule-id>:<sha256[0:8]>]\ so the location and rule are visible without exposing the secret. The 8-char hash gives forensic locating (\"this transcript references the same secret as that transcript\") without enabling recovery.
  • Configurable: ship a default rule set covering common cloud/SaaS API keys; let users add custom rules.

Why write-time, not session-end

  • Session-end runs after the transcript exists on disk. Anything that scrapes the file mid-session (backups firing on FS events, screen sharing, another tool reading ~/.claude/projects/) sees the raw secret.
  • Mid-session crashes leave the unredacted transcript in place indefinitely.
  • The current session is the operator's most active surface — the highest-value window to protect.

A SessionEnd hook (which we built locally as a workaround) is strictly weaker than write-time. We'd switch immediately if write-time were available upstream.

Feasibility evidence

We shipped a working SessionEnd hook in ~250 LOC of Python (hooks/session-end/redact-transcripts.py in our config repo) that walks ~/.claude/projects/<id>/*.jsonl, applies the 8-rule regex set, replaces matches with [REDACTED:<rule>:<sha256_8>], and writes atomically via tmp + os.replace. Idempotent. 6 unit tests passing. Audit log to ~/.claude/audit/redactions.jsonl. Runtime <1ms/MB.

The same logic at write-time would be smaller and more reliable. The jsonl writer already has the message boundaries; per-line regex scrub before flush is trivially streamable.

Labels

  • enhancement
  • security

Context

No values exposed in this issue. Happy to share the redactor source or our incident postmortem if useful — DM works.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗