Your agent has been writing your secrets to disk
I scanned 1.2 GB of my own Claude Code and Codex transcripts. Hundreds of credentials across dozens of files. Here is the script, and the four bugs that got past a green test suite.
TL;DR
Every coding agent keeps a full local transcript of every session, including all tool output. That means every env you ever ran is on your disk in plaintext, unversioned, and nobody is looking at it.
I wrote a script that walks the transcripts and replaces secrets with typed, hashed placeholders. On my machine it found a live Google API key, a plaintext password sitting next to my work email, a GCP service account key, 25 private key blocks, and 61 JWTs.
The problem nobody looks at
You ask your agent to debug a failing deploy. It runs env. It reads your .env. It runs a curl with a bearer token. All of that scrolls past, you fix the bug, you move on.
None of it is gone. It is in a JSONL file in your home directory, and it stays there. Mine going back a few months:
| Agent | Store | Files | Size |
|---|---|---|---|
| Claude Code | ~/.claude/projects/**/*.jsonl | 655 | 935 MB |
| Codex | ~/.codex/sessions/YYYY/MM/DD/*.jsonl | 211 | 170 MB |
| Kimi Code | ~/.kimi-code/sessions/**/wire.jsonl | 220 | 38 MB |
| Gemini CLI | ~/.gemini/tmp/*/chats/*.jsonl | 17 | 9 MB |
None of it is in git. No secret scanner runs over it. It is the largest pile of unaudited plaintext on a working developer machine, and it grows every day.
What was actually in there
373 findings across 105 files, out of 1,998 files and 1.2 GB scanned:
| Kind | Count | Where it came from |
|---|---|---|
| generic assignment | 148 | SOMETHING_KEY=value in shell and .env dumps |
| jwt | 61 | signed session tokens in support-portal links |
| google api key | 60 | one exported key, echoed across many sessions |
| private key | 25 | PEM blocks pasted into prompts |
| huggingface token | 25 | hf_ tokens |
| url password | 23 | scheme://user:password@host |
| atlassian token | 11 | ATATT |
| github token | 6 | ghp_ and gho_ |
| other | 14 | OpenAI, Anthropic, AWS, bearer, OAuth |
The Google key was the instructive one. It appeared 60 times, but it is one key: a single export GEMINI_API_KEY= line in a shell profile, copied into the transcript every single time an agent dumped my environment. One careless line, echoed for months.
Do not parse the schema
My first instinct was to target the fields where tool output lives. That is a trap. In Claude Code, toolUseResult is an object for Bash, a bare string for some tools, and an array for others. tool_result.content is a string or an array. The same output is stored twice per line, in two different shapes.
So the script does not model any schema. It parses each line and walks every string value. Adding an agent is a new glob, not a new parser. That one decision is why it covers four agents in one file.
The other thing it does not do is re-serialize. It finds secrets in the decoded value, then substitutes them into the raw line. Round-tripping a gigabyte through json.dumps would quietly rewrite every 1e-7 into 1e-07 across your whole history. Substituting into raw text means clean lines stay byte-identical and a changed line differs only where the secret was.
Four bugs my tests did not catch
My tests were green. Two of these fell out of running it against 1.2 GB of real transcripts. The other two came from deliberately attacking the design afterwards, which is the part I would have skipped if the first two had not shaken my confidence.
It was redacting source code
1,068 of the first 1,249 hits were not secrets. api_key = get_api_key() matched, capturing the function call. Because replacement is line-wide, it also rewrote def get_api_key(): elsewhere on the same line. It was corrupting code to scrub a secret that was never there. The fix: reject any candidate containing bracket characters, and require at least one digit, because real credentials essentially always mix letters and digits.
Showing context leaked the neighbour
The --show-context flag prints the text around a match with the secret replaced. But secrets cluster. An exported API key sits one line above the token, so printing 50 characters of context around one match displayed the other one in full. The function whose whole job was safe display was the one that leaked.
An escaped slash hid the secret
Some JSON encoders write / as \/. A secret containing a slash was detected, reported, and then not removed, because the spelling on disk matched none of the forms being substituted. The tool said it found something and left it there. That is the worst failure mode available: you believe the file is clean. The real fix was not the extra spelling, it was re-decoding the output and asserting no secret survived.
os.replace ate a symlink
Atomic writes use a tempfile and os.replace. On a symlinked transcript that replaces the link with a regular file and leaves the real one untouched and unredacted. This is reachable: five of my Claude home directories symlink to the same store.
What it will not touch
One rule keeps this safe: redact the record of what happened, never what the agent reads in order to operate.
An early draft wanted to scrub ~/.claude.json because it "probably has API keys in it". It does. They are the mcpServers.*.env keys your MCP servers need to start. Redacting them would break your setup to protect a file you were never going to share. Same for shell snapshots, settings, and every auth.json. It also skips anything modified in the last 60 seconds, so it will not rewrite the session you are sitting in.
Caveats
The JSONL is not the only copy. Codex mirrors every rollout item into thread_history.sqlite. Claude keeps pre-edit file copies in file-history/. OpenCode puts whole sessions in an 857 MB database. The script reports these and refuses to write to them, because corrupting an agent's live state database to scrub a secret is a bad trade. Scrubbing the transcripts is a real reduction in exposure, not a guarantee.
Regex detection has a floor. Prefixed tokens are easy. A high-entropy value assigned to a variable called config_blob is not distinguishable from a hash, and I would rather miss it than shred your transcripts.
Redacting is not rotating. A credential that has been sitting in plaintext for months should be considered compromised. This limits the blast radius of sharing the file. Rotating the key is what actually fixes it. Scrub the transcript, then go rotate.
The script
Single file, no dependencies, dry run by default. Nothing is written without --apply:
uv run redact_agent_transcripts.py # scan, write nothing
uv run redact_agent_transcripts.py --show-context
uv run redact_agent_transcripts.py --apply # rewrite, with backups Run it once on your own machine. I expect you will find something.