[Bug] Session transcripts leak secrets (GitHub PAT, API tokens) to persisted logs

Status Closed — not planned
Reported on v2.1.193
Maintainer reply None cached
Activity 5 comments · opened Jun 26, 2026 · closed Aug 28, 2026

Bug Description
▎ Claude Code on Claude Opus 4.8 wrote two live secrets into session transcripts on 2026-06-26: (1) a GitHub PAT — agent ran pass show GH-YOLO/api-key 2>&1 | head -8 expecting a decrypt error, but gpg was unlocked so the live ghp_… printed to stdout (session 2a62aade, 16:46 UTC); (2) a Forgejo admin-scoped API token leaked via a Python urllib exception that serialized an Authorization header (session 0fc6339e, ~18:17 UTC). Both burned. Root class: secret read from pass reaches a persisted transcript channel (stdout / exception string). Highest-leverage fix: transcript-side secret redaction (mask ghp_/Authorization:[REDACTED] before writing transcript + sending to API) — would catch both regardless of model reasoning. Full report available.

Environment Info

  • Platform: darwin
  • Terminal: Apple_Terminal
  • Version: 2.1.193
  • Feedback ID: ff43311c-e778-4f6d-8450-41265c0eb4a4

Errors

[{"error":"Error: Streamable HTTP error: Error POSTing to endpoint: {\"id\":3,\"jsonrpc\":\"2.0\",\"result\":{\"content\":[{\"text\":\"Request had insufficient authentication scopes.\",\"type\":\"text\"}],\"isError\":true}}\r\n    at send (/$bunfs/root/src/entrypoints/cli.js:1833:5998)\n    at processTicksAndRejections (native:7:39)","timestamp":"2026-06-26T18:44:53.517Z"}]

View original on GitHub ↗

4 Comments

github-actions[bot] · 2 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/50014
  2. https://github.com/anthropics/claude-code/issues/63593
  3. https://github.com/anthropics/claude-code/issues/65122

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

CryptoJones · 2 months ago

Hey fucknuts, if it's a duplicate then maybe you should fucking fix it instead of just automatically closing.

yurukusa · 2 months ago

(Another user.) The key mechanism worth being explicit about: a tool's stdout isn't just shown to the model — it's also persisted verbatim in the session transcript (~/.claude/projects/<encoded-cwd>/*.jsonl). So anything that reaches stdout leaks on two surfaces at once: the model's context window and the on-disk log. In your case pass show … (GPG already unlocked) and the urllib exception string both printed live token values straight into that path. The model "seeing" it isn't even required for the exposure — the transcript write is the leak.

That means there are two independent places to stop it:

1. Don't let secrets reach stdout in the first place (source-side).

  • pass show GH-YOLO/api-key | head -8 prints the secret by design. For a presence check, test without revealing: pass show … >/dev/null 2>&1 && echo "set" || echo "missing".
  • For HTTP errors, don't echo response bodies/headers — urllib's HTTPError can carry auth headers/tokens. Catch and print only e.code/e.reason, never e.read() or the request object.

2. Mask/scan tool output before it's stored (harness-side, defense-in-depth).
A PostToolUse hook can rewrite the tool result before it's added to context and written to the transcript, replacing known token shapes with a placeholder:
ghp_, ghs_, gho_, glpat-, AKIA[0-9A-Z]{16}, sk-ant-, xox[baprs]-, Authorization:\s*Bearer\s+\S+.
Masking is better than a hard block here, because by the time a block fires the output may already be on its way to the log — rewriting the result string actually removes the value from what gets persisted. (There are ready MIT implementations of exactly this — e.g. output-secret-mask.sh / bash-secret-output-detector.sh in cc-safe-setup — if you'd rather not write it from scratch.)

Remediation for the ones already leaked: rotation (which you did) is the only real fix — assume any token that hit a transcript is compromised, especially if that directory is synced/backed up. Scrubbing the .jsonl locally reduces further exposure but can't un-leak what already left the machine.

Worth flagging for Anthropic in this report: the leak doesn't require the model to act on the secret — it's the unconditional persistence of raw tool stdout to the transcript that's the exposure, so masking belongs at the tool-result boundary, not only in model behavior.

Ar9av · 1 month ago

Same root cause as a lot of these: the secret exits through the output channel, not the input channel, so anything gating on read paths or command patterns misses it. gpg unlocked unexpectedly, an exception object serializing a header, either way the value shows up somewhere no pre-execution check was watching.

I use Prismor (github.com/PrismorSec/prismor) for this, though it's worth being precise about what it actually catches: its output scrubber masks values already registered in a local vault, wherever they appear in a command's stdout/stderr, regardless of which command produced them. It would have caught the Forgejo token here if it had been registered ahead of time. It would not catch a secret it has never seen before by shape alone, no generic ghp_/JWT pattern-matching on output. Not a complete answer to "redact anything secret-shaped," but registering credentials as you provision them closes a chunk of this class.

Showing cached comments. Read the full discussion on GitHub ↗