claude-code - 💡(How to fix) Fix [security] Built-in secret redaction at jsonl write-time in ~/.claude/projects/

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

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

Root Cause

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.

Fix Action

Fix / Workaround

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.

RAW_BUFFERClick to expand / collapse

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:

FamilyPattern
Anthropic API keysk-ant-api03-…
OpenRouter API keysk-or-v1-<64 hex>
Stripe webhook signing secretwhsec_…
Supabase Personal Access Tokensbp_<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 PATgithub_pat_…
GitHub OAuth access tokengho_…

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.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix [security] Built-in secret redaction at jsonl write-time in ~/.claude/projects/