A couple of weeks ago I found a leaked password in one of my own repositories. It was in a README, in plain text, under the words “Vault password.” That file unlocks the encrypted configuration for a set of servers I run. So I wrote up what happened, committed a cleanup, and started rotating the credential on the live servers.
I got most of the way there before I figured out that nothing had leaked. The README held a file path: the place on disk where the real password is kept. But my own security tooling had blacked out that line before I ever saw it. All I saw was “Vault password” and a censored blob, and I filled in the rest myself.
I lost an afternoon to an emergency that didn’t exist. The tool I’d installed to prevent exactly this problem had invented it.
The safety net
My AI coding assistants read files all day, and files sometimes contain API keys. Everything they read lands in a transcript on disk, and those transcripts get backed up every night. A key that shows up once tends to stick around.
So I built two pieces:
- A live filter that masks anything that looks like a credential before it lands in the transcript.
- A nightly cleanup job that does the same to the transcripts already on disk.
I still think that’s a good idea. The problem was the phrase “anything that looks like a credential.”
8,270 false alarms
The filter was firing constantly: 82 times in three days, including on my Google Calendar events. So I looked at its rules.
Some rules matched shapes only credentials have, like the prefix Anthropic puts on its API keys. Others matched a word like “password” or “token,” followed by six or more characters of anything at all.
I ran both kinds across the roughly 22,000 files in my working folder. The precise rules matched 115 things. The loose rules matched 8,270.
Every sample I pulled was a false positive. ${NOTION_API_KEY} is a placeholder, not a key. My favorite: one line in the detection script itself, secret = m.group(grp), matched its own rule. The censor was censoring the censor.
What it cost
The noise buried the real warnings, because a real one looked like the other eight thousand.
It was also quietly rewriting my transcripts. The nightly job runs unattended at 2:30 in the morning. It never touched my source code. But a preview run showed what it did each night. It permanently replaced 14,052 pieces of ordinary text across 710 transcript files with black bars. Those transcripts are my record of months of work.
And it misled the AI, which is how I lost the afternoon. The assistant only sees the blacked-out version. So when it found a password-shaped blur under “Vault password,” it reported a leak. That was correct, given what it could see. I agreed, because the evidence looked solid.
The fix
The fix took about an hour. Precise rules still fire on sight. Loose rules now also check whether the match is obviously something else. A $ or ${} means a placeholder. Parentheses or brackets mean code. An all-caps environment variable name is a name.
Matches across my files dropped from 8,270 to 542. The nightly rewrites went from 14,052 to zero.
Before you turn on an alert
I’d argue a noisy guardrail isn’t a weaker version of a good one. When it acts on its own, it can be worse than nothing. With no alarm, you stay nervous, which is useful. An alarm that’s always wrong feels like coverage while it hides the real hits and invents fake ones. I’d rather run with no filter and know it.
Also, test your safety tools against the real case. My new rules passed the tests I wrote. Then I pointed them at the actual README line, and they missed a credential-shaped value written exactly that way. I’d taught the filter to skip anything in backticks, which is exactly how a person writes a password in a markdown file.
There are 41 cases in the test suite now, and 23 of them come word for word from things this tool got wrong.
The net is still running. I trust it more now, mostly because it says a lot less.
