Is your .env file exposed? How to check (and what to do)
Your .env file holds the passwords and keys your app runs on. It's meant to stay on your machine and your server. If the wrong folder gets deployed, it can end up one URL away from anyone who guesses the address.
Want the quick answer first? The free ShipWatch scan requests /.env and /.git/HEAD on your live domain and tells you if either one is publicly downloadable. It's read-only and needs no signup.
The problem: anything in the deployed folder can be downloaded
A web host serves the files in one folder. If a file is in that folder, anyone who asks for its path gets it. There's no "private" setting for a file that's been published. Two files cause the most damage when that happens:
.envusually holds database passwords, API secret keys and tokens. Lines look likeSTRIPE_SECRET_KEY=sk_live_....- The
.gitfolder is your repository's internal database. If it's served, your source code and its history (including secrets you deleted in a later commit) may be reconstructable from it.
Neither file should ever be deployed. They get there in a few common ways:
- The whole project folder gets published instead of the build output. Hosts deploy one folder. Netlify calls it the publish directory, Vercel calls it the Output Directory. Vercel's docs note that with the "Other" framework preset and no
publicfolder, it serves files from the project's root directory, which is where.envand.gitlive. - A
.envfile was put in thepublicfolder. Vite copies everything inpublicto the root of the build output as-is, and Next.js serves files inpublicfrom the base URL. So a file atpublic/.envcan end up atyourapp.com/.env. - The project was uploaded or cloned onto a server as-is, for example by copying the repository into the web root over FTP, or running
git clonethere. The.gitfolder and any.envcome along. - A static-asset upload includes more than it should. On Cloudflare Workers, everything in the configured assets directory is uploaded unless it's listed in an
.assetsignorefile.
Publishing files that shouldn't be public is a classic case of what OWASP calls security misconfiguration. It's not a clever attack. Someone only has to type the URL.
How to check your app yourself
Run these against your live domain, not localhost. Only check sites you own or are allowed to test.
1. Ask for the file directly
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" https://yourapp.com/.env
curl -s https://yourapp.com/.env
Read the content, not just the status code. Many hosts (especially for single-page apps) answer any unknown path with your homepage and a 200. If the second command prints HTML, that's your homepage, not a leak. If it prints lines like DATABASE_URL=... or OPENAI_API_KEY=..., the file is public.
2. Try the common variants
Frameworks load several env files, such as .env.local and .env.production (see Vite's list). Check each one:
for f in .env .env.local .env.production .env.production.local .env.development; do
printf "%-24s " "$f"
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" "https://yourapp.com/$f"
done
Anything that returns 200 with a text/plain or application/octet-stream type deserves a closer look with curl -s.
3. Check for a public .git folder
curl -s https://yourapp.com/.git/HEAD
curl -s https://yourapp.com/.git/config
If the first command prints ref: refs/heads/main (or another branch name), or a long hexadecimal commit ID, your .git folder is being served. If the second prints a block starting with [core], same result. An HTML page or a 404 is what you want to see.
4. Look at what you actually deploy
Run your build locally and list the hidden files in the output folder (dist, build or out, depending on your tool):
npm run build
find dist -name ".*"
If this prints .env, .git or any other dotfile you didn't intend to publish, it will be deployed. Also open your host's build settings and confirm the output/publish directory is the build folder, not the project root.
5. Check whether .env was ever committed
git ls-files | grep -i "\.env"
git log --all --oneline -- .env
If either prints anything other than an example file like .env.example, the real file is (or was) in your repository. Adding it to .gitignore now won't remove it: gitignore doesn't affect files Git already tracks.
How to fix it
1. Stop publishing the files
Point your host at the build output only, remove any .env from public, and redeploy. Then paste this into your AI tool:
My .env file (or .git folder) is publicly downloadable on my live site. Fix the deploy so only the build output folder is published (dist, build or out, not the project root). Remove any .env file from the public folder. Add .env, .env.* and *.local to .gitignore, keep a .env.example with blank values, and if .env is already tracked by git, untrack it with git rm --cached. Tell me exactly what to change in my host's build settings, then list every variable that was in the file so I can rotate them.
2. Block dotfiles at the server too
Fixing the deploy is the real fix. A server rule is a second layer in case a dotfile slips into the output again. Keep /.well-known/ reachable, since standard services use that path (RFC 8615).
# nginx (inside the server block)
location ~ /\.(?!well-known) { deny all; }
# Apache (.htaccess or vhost)
RedirectMatch 404 "/\.(?!well-known)"
# Cloudflare Workers static assets: .assetsignore in the assets directory
.env*
.git
References: nginx deny, Apache RedirectMatch, Cloudflare .assetsignore.
Add a server-level rule for my host that returns 404 (or 403) for any request to a path starting with a dot, such as /.env, /.env.local and /.git/HEAD, while still allowing /.well-known/. Show me the exact config file and where it goes, and give me curl commands to confirm it works after I deploy.
3. Rotate every key that was in the file. This step isn't optional.
Once a file has been public, assume someone has a copy. Removing it from the server doesn't revoke anything in it. For each value in the exposed .env, create a new key or password in the provider's dashboard, put the new value in your host's environment settings, deploy, then revoke the old one. The OWASP Secrets Management Cheat Sheet covers rotation and revocation in more depth.
- Supabase: follow Supabase's steps for a leaked secret or service_role key, and change the database password if it was in the file.
- Stripe: rotate secret and restricted keys from the API keys page (Stripe docs).
- AI providers, email, storage and everything else: create a new key in each dashboard and delete the old one.
My .env file was publicly downloadable, so every value in it must be treated as leaked. Make a checklist of every variable in it with the provider it belongs to and where in that provider's dashboard I create a new key or password and revoke the old one. Mark which ones are the most dangerous (database passwords, service_role or secret keys, payment keys) so I rotate those first. Don't print any secret values.
4. If .git was public, treat your history as public
For a .git-only deep dive (how a repository is rebuilt from a web server and per-host block rules), see .git folder exposed.
Any secret that was ever committed, even one deleted later, should be rotated. GitHub's guide on removing sensitive data makes the same point: revoke or rotate the secret first. Rewriting history is a separate, optional cleanup with real side effects.
5. Look for misuse and confirm the fix
Check each provider's usage and billing pages for activity you don't recognize, and your host's access logs (if it keeps them) for requests to /.env or /.git. Then re-run the curl commands above. You want HTML or a 404 for every one.
Make it hard to happen again
- Keep secrets in your host's environment variable settings, not in files inside the project you deploy.
- Commit a
.env.examplewith blank values, never the real file. Vite's docs recommend adding*.localto.gitignore. - Never put
.envinpublic, and don't let an AI tool move it there to "fix" a missing variable. - Remember the separate problem: a variable with a
VITE_orNEXT_PUBLIC_prefix ends up inside your JavaScript even when.envitself is private. See API key exposed in frontend. - Re-check the live site after each deploy, since a change to build settings can quietly change what gets published.
What the free ShipWatch scan checks here (and what it doesn't)
The scan requests /.env and /.git/HEAD at the root of your live domain without following redirects. It reports a public .env as critical only when the response is a 200 containing KEY=value lines and not an HTML page, so a homepage fallback doesn't count. It reports a public .git folder when /.git/HEAD returns a ref: refs/... line. Each finding comes with a fix prompt.
It doesn't check variants like .env.local, files in subfolders, a detached-HEAD .git folder, or your repository's history, so use the steps above for those. The same scan also looks for leaked keys in your page and JavaScript, missing security headers and a missing HTTPS redirect. For the rest of the pre-launch list, see the vibe coding security checklist.