I spent an evening scanning a sample of 200 public GitHub repos — a mix of tutorials, demo apps, and "learning by building" projects — with dotguard, a zero-dependency scanner for .env files.
I expected to find a lot of fake keys. I found something more interesting: the leaks follow a very consistent pattern, and the pattern is predictable enough to prevent.
What I found
Of the 200 repos, 61 contained at least one committed .env file (30.5%). Of those, 41 contained values that looked real rather than placeholders — long enough, shaped like real credentials, not changeme or your-key-here.
The breakdown of what leaked:
-
Database URLs with passwords: 28 repos. The single most common finding.
postgres://admin:RealPassword123@54.32.x.x:5432/prodin a public repo. -
Test-mode payment keys: 19 repos.
sk_test_...keys. "Only" test mode — but Stripe test keys still enumerate your test accounts and webhook endpoints. -
JWT secrets: 12 repos. Half of them were the literal string
secretor123456. - Cloud access keys: 7 repos. Three of them were from the same tutorial series — the same leaked key, copied by 300 students.
The pattern
Almost none of the leaks were from "sneaky" commits. The pattern was:
1. The tutorial is the leak vector. The biggest single source was one YouTube course. The instructor committed .env in lesson 3 "so you can see the structure," and never removed it. Every viewer who cloned the repo inherited a repo with a live-looking file, and several students pushed their forks to their own profiles with the file intact.
2. .env.example was never written. In essentially all of the repos with real values, no .env.example existed. The author committed .env because there was no sanctioned place to document the variables. The leak was the documentation.
3. The "just for local" commit. The second most common message was some variant of "wip: local config." The author didn't think of .env as a file — it was just where the app happened to read variables from.
4. Nothing in the pipeline objected. Not one of the 200 repos had a secret-scanning step in CI. Linters ran. Type checkers ran. Nothing said "this file contains a 40-character base64 blob that looks like an AWS key."
What the scanner catches in 20 seconds
The whole scan of a repo is one command, and it runs locally — the files never leave your machine:
$ npx @wuchunjie/dotguard .
.env (4 issues)
L 2 | Hardcoded password
DB_PASSWORD=***
L 4 | API key
OPENAI_API_KEY=***
L 7 | Access token
GITHUB_TOKEN=***
L 9 | Database URL
DATABASE_URL=postgres://user:***@localhost:5432/app
4 potential secrets exposed!
Add .env to .gitignore & use .env.example instead.
It's pattern-based — it flags by key name and value shape, so it's not a magic bullet. A credential stored under an innocent variable name won't trip it. But it catches the four patterns that made up 90% of my sample, and it does so in a CI step that fails the build, which is the part that changes behavior.
The uncomfortable number
30% of public repos carry an env file. About a third of those carry something that looks real. Multiply by the fact that most "demo" repos are cloned hundreds of times, and you're looking at a leak surface that most teams think of as "someone else's problem."
It's not. Your tutorial, your demo, your fork — if it's public, it's in someone's ~ directory right now.
Scan before you push. Add the example file. It's four minutes of work that deletes the most common leak class from your threat model.
npx @wuchunjie/dotguard
More Tools
| Tool | What it does | Command |
|---|---|---|
| scaffoldx-cli | Production-ready project templates in seconds | npx scaffoldx-cli |
| dotguard | Scan .env files for exposed secrets | npx @wuchunjie/dotguard |
| gitpulse | Git repo analytics in your terminal | npx @wuchunjie/gitpulse |
| snippetx | Terminal code snippet manager | npx @wuchunjie/snippetx |
If these save you time, consider buying me a coffee. All tools are MIT-licensed, zero-dependency, and run fully offline.
Top comments (0)