The word "secretaria" contains "secret"
tidy.
Writing
The word "secretaria" contains "secret"
Sebastián Díaz Thomas · Santiago, Chile · 18 August 2026
There are good credential scanners and I did not need to write one. gitleaks and trufflehog are mature, well tested, and aimed at a git repository, where a leaked key is a commit that will outlive the mistake.
The thing I wanted to scan was a Downloads folder.
That is a different problem than it first looks. Nobody reviews a Downloads folder. A config file you were sent on Slack in March, a .env a contractor mailed you, a server export you opened once — they land there, they are never opened again, and they are never deleted either. The folder is a sediment. And because it is not a repo, nothing scans it, ever.
So I added a check to a Mac app I make. It reads the plain text files in the folders you watch and tells you if a password or an API key is sitting in one of them. It never shows the secret itself, only the file and what kind of thing it found, because a scanner that prints the key into a log has invented a second copy of your problem.
Writing it took an afternoon. Getting it to shut up took considerably longer, and that part was the education.
The misses were boring, except for the underscore
The high-confidence half is just shapes. An AWS access key ID is AKIA and sixteen uppercase alphanumerics. A GitHub token is ghp_ and thirty-plus characters. OpenAI is sk-, Slack is xox, Stripe is sk_live_. A private key announces itself in capital letters. These are unambiguous and there is nothing to think about.
The interesting half is the assignment: some word that means secret, then a separator, then a value. And my first attempt at that missed the single most common real case in the world.
db_password = Tr0ub4dor3xKz9
I had written \bpassword. The word boundary never fires, because in every regex flavour that matters the underscore is a word character. db_ and password are the same word as far as \b is concerned, so there is no boundary between them to match.
Which means the pattern found password = hunter2 and skipped db_password = hunter2, and it is the second one that appears in real configuration files. I did not notice by reading the code. I noticed because a test failed.
The same shape bit me from the other side:
aws_secret_access_key = wJalrXUtnFEMIK7MDENG
Here the keyword is secret, and it is followed by more words before the equals sign. So the pattern needed to allow a suffix chain after the keyword. I wrote that, felt clever, and shipped a scanner that cried wolf at everything.
Then it flagged my secretary
The suffix chain I had written was, roughly, "the keyword, then any more word characters, then the separator". Which is how I ended up with this in the results:
secretaria_email: maria@empresa.cl
"Secretaria" is Spanish for secretary. It starts with the letters s-e-c-r-e-t.
I am fairly sure this bug does not exist in any scanner written by someone working in English, because nobody working in English has a common noun that begins with the word secret. Mine flagged a contact list as a credential leak, in a file that contained no credentials at all, because of a word I have typed ten thousand times in my life without ever noticing it was hiding another one.
The fix is small and slightly humbling: the keyword has to be followed by an actual separator — an underscore or a hyphen — not by more letters. aws_secret_access_key passes. secretaria does not.
Once I started looking, the file was full of them.
tokens: 15000000<br>token_expira: 2026-08-18<br>api_key_docs: https://ejemplo.com/docs
A count of tokens is not a token. A date is not a token. A link to the documentation about an API key is not an API key. All three matched, and every one of them is the kind of line that shows up in an ordinary YAML file that has nothing to hide.
So the value side needed guards too: not a bare number, not an ISO date, not something that starts with http.
Half the .env files in the world are placeholders
The other large category is files that are about secrets without containing any.
API_KEY=your-api-key-here<br>PASSWORD=changeme<br>API_KEY=""
Every example repository ships one of these. If your scanner treats them as findings, then the first thing a person sees when they turn the feature on is a screen of alarms about a tutorial they downloaded, and they turn it off and never turn it back on.
There is also a minimum length, which took me two tries to get right. password = 12345 is not a credential, it is a joke or a placeholder. But eight characters is not much of a filter either, and I know it: monkey = 12345678 passes length and fails common sense. I catch that one only because the value is all digits.
Here is where it ended up. It is not elegant and I am not going to pretend it is.
(?i)(password|passwd|api[_\-]?key|secret|token)<br>(?:[_\-][A-Za-z0-9]+)*<br>\s*[:=]\s*["']?<br>(?!(your|xxx|placeholder|changeme|example|
Every clause in that...