My Search Couldn't See the Secret, Then We Pasted It Into the Fix

My Search Couldn't See the Secret, Then We Pasted It Into the Fix
My assistant keeps notes between sessions. About a hundred small files of them. I decided they should live in a private Git repo, so they'd have history and sync between my machines.
Before pushing, I asked the obvious question. Is there anything in here that shouldn't be?
The assistant scanned the files, found one password, moved it out, and confirmed it was gone. Clean.
Both halves of that were wrong. The scan couldn't see the file it was checking. And a few minutes later the real password went straight back into the repo, inside a brand new file.
A search that skips what you ask about
The assistant moved the password into a separate credentials file and added that file to .gitignore. Exactly the right move. Then it checked:
grep -rn 'the-password' . # no match
No match. Looks clean.
Except grep on my Mac isn't the grep you think it is. It's a shell function that wraps a faster search tool, ugrep, and that tool respects .gitignore by default. A recursive search quietly skips any ignored file.
The assistant had just put the secret into an ignored file. So of course the recursive search didn't find it. Ask about the file directly and there it is:
grep -n 'the-password' creds.txt # line 6
In this case the result happened to be right for the repo, since an ignored file doesn't get committed. But the reasoning was broken. The check was skipping the exact place the secret lived, and calling that "absent."
A recursive no-match is not evidence of absence. It's evidence the search didn't look.
Plenty of modern search tools do this. ripgrep does it. ugrep does it in some setups. Editors' project-wide search often does it. That's a great default when you're hunting for code, and a dangerous one when you're hunting for something that must not exist.
Then the fix leaked it
This is the part that made me laugh, a bit grimly.
Having learned that the search skips ignored files, my assistant did the conscientious thing. It wrote a note about the behavior so it would never be fooled again. And for the example in that note, it used the real password.
The documentation of the leak was the leak.
It committed that file. Nothing flagged it, because the scan had already "passed."
It was caught by luck, twice over. The push to the new repo had been blocked seconds earlier by a permission check for a completely unrelated reason. And the assistant's next verification step happened to search the committed files rather than the folder, which matched its own brand new note.
Neither of those was process. If either had gone the other way, the password would have been in Git history, and a secret in history is a secret you rotate, not one you delete.
Check the thing you're about to publish
What went wrong is that both checks were aimed at the working folder. But the working folder isn't what gets pushed. The commits are.
So check the commits:
git grep 'the-password' HEAD # everything in the latest commit
git log --all -S 'the-password' # every commit that ever added or removed it
git grep against a commit searches what Git has actually stored. It doesn't care about .gitignore, your shell aliases, or which search tool is in fashion. If it's in the commit, it shows up.
And the second lesson is about timing. The assistant verified once, at the start, and then kept writing files. Every write after a check makes the check stale. The only check that counts is the last one before the push.
Writing about a secret is writing the secret
The second mistake is the one I find most interesting, because it's so human.
When you document a problem, you reach for the real example. It's right there, it's concrete, and it makes the note clearer. That instinct is good almost everywhere. With credentials it's exactly backwards.
An AI assistant is particularly prone to this. It had the real value in front of it from a few minutes earlier, and "use the actual example" is a reasonable default for a writer. Nothing in the moment marked that string as radioactive. It had just been redacted, which in the assistant's head meant "dealt with."
So the rule I use now is blunt. When documenting a secret you just removed, the example is always a placeholder. the-password, sk_live_XXXX, hunter2. Never the real one, not even in a private note, not even "just for now."
What I do now
Before any push from a repo that has ever been near a credential:
- Search the commit, not the folder:
git grep '<value>' HEAD. - Search the history too:
git log --all -S '<value>'. - Run it after the last write, not at the start of the cleanup.
- Don't trust a recursive search's silence until you know what it skips.
None of this is clever. It's two commands and a habit.
The scary part of this story isn't that a tool had a surprising default. Tools always will. It's that a "clean" result felt like evidence, and the confidence from that one check covered the very next mistake.