We committed a .env to a public repo and did not notice for seven months
Sixteen live credentials, public from January, found in August. What we did in what order, and the three checks that would have caught it on the first push.
On 26 August 2026 we ran an inventory across all of our repositories. One of
them, a side project called viral-hook, was public and contained
backend/.env with sixteen live credentials in it: Stripe keys, a database
connection string, OpenAI, SendGrid, Resend, Google OAuth, and the Django secret
key.
The commit that added it was dated 4 January 2026. It had been public for roughly seven and a half months.
This site argues that systems should be able to explain themselves. That argument is worth less if we only publish the parts where we got it right.
What happened
The repository was a personal project that was never meant to be seen. The
.env went in early, during setup, before the .gitignore had been thought
about — the specific sequence being git init, write some code, get it working,
git add ., and only later add a .gitignore that then had no effect on a file
already tracked.
That is the mechanism, and it is worth naming precisely because it is so
ordinary: .gitignore does not untrack a file that is already tracked.
Adding .env to .gitignore after the fact makes the file invisible in git status while it continues to be committed with every change. The safety
mechanism appears to be working and is not.
Nothing about this was exotic. No compromised dependency, no clever attack. A file was added before a rule existed to prevent it, and then the rule made it harder to see.
What we did, in order
The order matters more than the individual steps, and it is the opposite of what instinct suggests.
1. Rotated every credential first. Before touching git history, before making the repository private, before anything else. Sixteen keys, each replaced at the provider.
This is the part people get backwards. The instinct is to delete the file and clean the history, because that feels like fixing it. It is not fixing it — a public repository has been cloned, forked, mirrored and scraped, and the credentials are already elsewhere. History rewriting protects against future discovery. Rotation is the only thing that closes the actual exposure, and every minute spent on history is a minute those keys remain live.
2. Made the repository private. Second, not first, for the same reason. It reduces further discovery and does nothing about what has already been taken.
3. Checked for use. Stripe dashboard for unrecognised API activity, database logs for connections from unfamiliar addresses, the mail providers for sends we did not initiate, OpenAI for usage we could not attribute.
We found no evidence of misuse. That is not the same as proving there was none — absence of evidence in logs with finite retention is weak evidence, and seven months is longer than several of those retention windows. The honest statement is that we found nothing, not that nothing happened.
4. Audited every other repository. This is the step that turns an incident
into a fix. authority-wizard and everly also had committed .env files.
Both were private, so the exposure was very different in kind, but the same
mistake had been made three times and would have been made a fourth.
The three checks that would have caught it
Each of these would have stopped this on the first push. Together they cost about an hour to set up across all repositories.
A pre-commit hook that blocks secrets. Local, immediate, and it fails before anything leaves the machine:
# .git/hooks/pre-commit — or better, managed via pre-commit / husky so it is
# in the repository rather than on one developer's disk.
if git diff --cached --name-only | grep -qE '(^|/)\.env(\..*)?$'; then
echo "Refusing to commit a .env file."
echo "If this is genuinely intended, use: git commit --no-verify"
exit 1
fi
Crude and effective. A dedicated scanner — gitleaks, trufflehog,
detect-secrets — does the same job against credential patterns rather than
filenames, which catches the key pasted into a config file or a notebook.
Provider-side secret scanning. GitHub scans public repositories for known credential formats and notifies both you and the provider, which will often revoke the key automatically. It is free and it is on by default for public repositories.
We should have received such a notification in January. We cannot now reconstruct whether one was sent to an address nobody read or whether the formats did not match — and that uncertainty is itself the finding. An alert nobody receives is not a control. Ours went to an inbox that was not being monitored.
A .gitignore before the first commit. The cheapest of the three. Every
framework's init produces one; the failure was starting from an empty directory
and adding it later.
What we changed
Beyond the three checks above:
- A quarterly repository inventory. Every repository, public or private,
checked for tracked
.envfiles and for the last rotation date of anything it references. That inventory is how this was found in the first place, and it is now a recurring task rather than something that happened once. - Alerts go somewhere monitored. Security notifications now go to an address a person reads daily.
- Credentials are scoped and short-lived where the provider allows it. A restricted, read-only, expiring key exposed for seven months is a materially smaller event than a live secret key.
Why publish this
Two reasons, and the first is the weaker one.
The mistake is common. Committed .env files are one of the most frequently
found classes of exposed secret, precisely because the sequence that produces
them is the natural way people start a project. Writing up what we did in what
order is genuinely useful to somebody who finds themselves in the same position
at 11pm and is about to start rewriting git history instead of rotating keys.
The second reason is the one that matters. Most of this site consists of us explaining how to build systems that are honest about their own failures — that refuse rather than guess, that record what they did, that surface a discrepancy rather than quietly choosing a side.
A firm that argues for that standard and publishes only its successes is making a weaker argument than it appears to be. This is a real failure, on our own infrastructure, found by our own audit, with the dates attached.
The thing worth taking from it is not that we were careless once. It is that the control which would have prevented it was cheap, obvious, and not in place — and that the alert designed to catch it was pointed at an inbox nobody read. Both of those are true of a great many teams right now, including some who are confident they are fine.
More in the case studies and the AI reliability audit.