[Bug] Session transcripts leak secrets (GitHub PAT, API tokens) to persisted logs
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"}]Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Hey fucknuts, if it's a duplicate then maybe you should fucking fix it instead of just automatically closing.
(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 casepass 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 -8prints the secret by design. For a presence check, test without revealing:pass show … >/dev/null 2>&1 && echo "set" || echo "missing".HTTPErrorcan carry auth headers/tokens. Catch and print onlye.code/e.reason, nevere.read()or the request object.2. Mask/scan tool output before it's stored (harness-side, defense-in-depth).
A
PostToolUsehook 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.shin 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
.jsonllocally 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.
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.