How to detect exposed .env files in a GitHub repository
By Seaworthy · May 11, 2026
Photo by Christina @ wocintechchat.com on Unsplash
Why .env files keep ending up on GitHub
I've seen this happen so many times that I've stopped being surprised by it. A developer creates a .env file locally, writes some database credentials into it, and then at some point, accidentally or through muscle memory, git add . gets run. The file is committed. The push goes up. Five minutes later those credentials are sitting in a public repository, indexed by GitHub's search, and possibly already scraped by one of the automated tools that trawl for this sort of thing.
Sometimes it is worse than that. The .env file gets removed from the working tree but it is still in git history. The developer thinks the problem is solved. It is not.
This article covers how to detect the exposure, how to actually clean it up, and how to make sure it does not happen again.
Finding a .env file in current repository state
The first thing to check is whether a .env file exists anywhere in the current HEAD of the repository.
git ls-files | grep '\.env'
This only shows tracked files at HEAD. It is the fastest check and catches the most common case: someone forgot to add .env to .gitignore before their first commit.
If you want to be more thorough and check all subdirectories:
find . -name "*.env" -not -path "./.git/*"
That will also catch files like .env.local, .env.production, and .env.staging, which are just as dangerous.
Searching git history for .env files
This is where most people stop looking, and it is a mistake. Removing a file from HEAD does not remove it from history.
git log --all --full-history -- "**/.env"
If that returns commits, those commits contain the file and its contents. Anyone who has cloned the repo, or anyone who uses GitHub's API, can retrieve them.
To see the actual content at a specific commit:
git show <commit-hash>:.env
And to get a full picture of what secrets were in the file at that point:
git log --all -p -- .env
The -p flag shows the diff for every commit that touched .env. Scroll through it and you will quickly see if credentials were ever committed.
Searching GitHub with the API
If you are auditing a GitHub organisation rather than a single repo, GitHub's code search can find .env files across repositories. This is also how attackers do it.
gh search code --filename .env "DB_PASSWORD" --owner your-org-name
The GitHub CLI (gh) wraps the search API cleanly. You can substitute DB_PASSWORD for any secret pattern you are looking for: SECRET_KEY, API_KEY, STRIPE_SECRET, and so on.
Be aware that GitHub code search indexes the current default branch. It will not surface secrets that have been removed from HEAD but still live in history.
Actually removing secrets from history
Removing a file from the current commit is not enough. You need to rewrite history, which is disruptive on any branch other people are using. There are two main tools.
git filter-repo is the current recommendation. It is faster and safer than the older git filter-branch:
pip install git-filter-repo
git filter-repo --path .env --invert-paths
That rewrites every commit in the entire repository history to exclude .env. After this, you must force-push every branch and every tag. Anyone who has cloned the repository will need to re-clone.
GitHub also has a "cached views" problem: even after a force-push, the old content may be served from GitHub's cache for a short window. Contact GitHub support to request a cache flush if you are dealing with a genuinely sensitive exposure.
Whatever you do: rotate the credentials first. Do not wait until the history is clean. The secrets are already out; assume compromise.
Preventing it in the future
The cleanest prevention is a .gitignore entry before the first commit:
# Secrets
.env
.env.*
!.env.example
The !.env.example line explicitly allows committing a template file with placeholder values, which is good practice for documenting what variables a project needs without leaking the actual values.
Beyond .gitignore, a pre-commit hook can catch the file even if .gitignore is misconfigured. The pre-commit framework has a detect-secrets hook that scans for high-entropy strings and common credential patterns before the commit lands.
pip install pre-commit detect-secrets
Then in .pre-commit-config.yaml:
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
This will not catch every possible secret format, but it catches the most common ones: AWS keys, Stripe keys, generic high-entropy strings, and common variable names like password and secret.
What actually gets leaked
People often assume an exposed .env file means a leaked database password and nothing else. In practice a single .env file frequently contains: the database URL with username and password, a JWT signing secret, third-party API keys (Stripe, SendGrid, Twilio), an S3 bucket name with access credentials, and sometimes admin panel URLs or internal service endpoints.
That is not a list of three things for rhetorical effect. It is a literal inventory of what I have seen in real .env files found via GitHub search.
The blast radius of one committed .env file can be enormous. Which is why the rotation step matters so much. Do not clean the repository and call it done. Treat every secret in that file as compromised.
Seaworthy's data exposure checks flag committed .env files, high-entropy strings in source code, and hardcoded credential patterns as part of its static analysis scan.
This article was generated by AI and summarises publicly available sources.
Seaworthy scans your repo for the issues covered in articles like this one.
Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.