Is my .env file public? How to check and fix it
By StackSiren · Published
Request https://yourdomain/.env and read the response body, not only the status code. If you get real KEY=value lines, treat every secret in that file as compromised: rotate them, remove the file from the deploy, and stop shipping .env with your build.
A status of 200 on a single-page app is often a false alarm (the host returns index.html for every path). A body that looks like your secrets is never a false alarm. This post walks through the check, the usual causes on Lovable, Bolt, Vercel and Replit deploys, and the rotate-then-fix order. Background: the exposed .env guide.
How a .env ends up on the public web
A .env file is only private if nothing serves it. Common ways it becomes downloadable:
- It was copied into the static output. Vite copies everything in
public/to the root ofdistas-is. A.env(or.env.production) dropped intopublic/is then available at/.env`. - The host serves the whole project folder, not just
distor.next. Self-hosted nginx/Caddy setups and some DIY Docker images do this by accident, which can also expose/.git/config. - The file was committed and the deploy includes the repo root. Some CI configs upload more than the build artefact. Vercel's CLI skips
.gitand some.env*.localnames by default, but a plain.envis not automatically excluded — add paths to.vercelignorewhen needed. - A backup or misnamed copy (
env.txt,.env.bak,.env.old) sat next to the site files.
Separately, variables with a VITE_ or NEXT_PUBLIC_ prefix are baked into JavaScript even when .env itself is private. That is a different leak path: are VITE_ variables secret?.
Step 1: Check with curl (body matters)
Only against sites you own:
SITE=https://yourapp.com
# Status alone is not enough
curl -s -o /tmp/env-body -w "HTTP %{http_code}\ncontent-type: %{content_type}\n" "$SITE/.env"
head -20 /tmp/env-body
echo "---"
# Same idea for common variants
for path in /.env /.env.local /.env.production /.env.production.local /.git/config; do
code=$(curl -s -o /tmp/probe -w "%{http_code}" "$SITE$path")
# Rough HTML vs KEY=value detection
if head -c 200 /tmp/probe | grep -qE '^[A-Za-z_][A-Za-z0-9_]*=' ; then
echo "$path -> $code LIKELY ENV CONTENTS"
elif head -c 15 /tmp/probe | grep -qi '<!DOCTYPE\|<html'; then
echo "$path -> $code HTML (SPA fallback, not a leak by itself)"
else
echo "$path -> $code (inspect /tmp/probe)"
fi
done
Interpret the result:
| What you see | Meaning |
|---|---|
404 / 403 / empty |
Fine for that path |
200 + HTML homepage |
SPA catch-all — not a .env leak |
200 + KEY=value lines |
Leak — rotate and fix |
200 + [core] in /.git/config |
Git folder exposed — treat history as readable |
You can also open https://yourapp.com/.env in a browser. If it downloads or shows secret-looking lines, same conclusion. Our env file checker automates the /.env and /.git/config probes on domains you own without displaying secret values.
Step 2: SPA 200 false positives
Many static hosts (Netlify, Cloudflare Pages, some Vercel SPA setups) rewrite unknown paths to index.html with status 200 so client-side routing works. That means:
curl -w "%{http_code}"returning200for/.envproves nothing by itself.- Always inspect
Content-Typeand the first bytes of the body. - A real
.envis almost nevertext/html. Look fortext/plainor an empty/octet type plusNAME=valuelines.
If the body is your app shell, move on — then still search the JavaScript bundle for secrets, because a private .env can still have fed VITE_* values into the build.
Step 3: Fix per host (stop serving the file)
Do these in parallel with rotation (next section). The goal is: deploy only build output, never the env file.
Every Vite / static project
- Add to
.gitignore:.env,.env.*,!.env.example. - Never put secrets in
public/. - Commit
.env.examplewith placeholder names only. - Deploy
dist(or your framework's output), not the repo root.
Vercel
- Keep secrets in Project → Settings → Environment Variables (Production / Preview / Development as needed).
- Prefer server-only names (no
NEXT_PUBLIC_/VITE_prefix) for anything sensitive. - If a
.envmust exist locally, list it in.vercelignoreso CLI uploads skip it. Dashboard env vars are the source of truth for deploys. - After a leak, rotate values in the dashboard and redeploy so old builds are no longer the live ones.
Netlify / Bolt-on-Netlify
- Store secrets in Site configuration → Environment variables for Functions.
- Publish directory should be the build output (for Vite, usually
dist). - Netlify secret scanning can fail a build when it finds values from variables marked secret in the output — useful, not a substitute for keeping
.envout of the publish folder. - Ask Bolt to keep keys under Secrets for server functions, not in client env files.
Lovable
- Keep API keys in Lovable Secrets (server / Edge Function side). Lovable treats
VITE_values as browser-exposed build-time config. - If you connect GitHub and deploy elsewhere, recreate secrets on that host; don't copy a
.envintopublic/.
Replit
- Use Replit Secrets and read them only in server code.
- Don't rely on a
.envsitting in the workspace if the deployment serves the project tree. Point the web server at the build output and block dotfiles if you run a custom server.
Self-hosted (nginx example)
# Block dotfiles except ACME /.well-known/
location ~ /\.(?!well-known) {
deny all;
}
Serve only the static build directory as the document root.
Step 4: Rotate every secret that was in the file
Assume anything in a public .env was copied. Work through the file (from your local copy or password manager — don't paste secrets into random websites):
- Provider keys (OpenAI, Anthropic, Stripe secret, Resend, Twilio, …): revoke/create in each dashboard. See leaked OpenAI API key for the first-hour pattern.
- Supabase: if a
service_role/sb_secret_value was present, follow My Supabase service_role key is exposed. Publishable/anon keys are public by design but still review RLS. - Database URLs, JWT secrets, webhook signing secrets: rotate at the source, update server-only env vars, redeploy.
- Put replacements only in server-side config (host env vars, Edge Function secrets). Do not recreate a public
.envon the CDN.
Also check git history if the file was ever committed:
git ls-files .env .env.*
git log --all -- .env
Rotating the keys is what makes an old commit harmless. Rewriting history with git filter-repo is optional and disruptive; do it only after rotation if the repo is or was public.
Step 5: Confirm clean
Re-run the curl checks. Confirm /.env no longer returns KEY=value content. Spot-check the live JS for secret-shaped strings with the exposed API key checker or a manual Sources search.
StackSiren's free scan probes /.env and /.git/config on sites you own and flags likely env bodies without showing the values. A paid $9 single report is available when you want a fuller pass you can keep; ongoing monitoring starts at Solo ($29/mo).
Related reading
- Exposed .env and .git guide
- Are VITE_ environment variables secret?
- I leaked my OpenAI API key. What should I do now?
- My Supabase service_role key is exposed. What now?
FAQ
curl says 200 — is that always a leak?
No. Many SPAs return 200 + index.html for unknown paths. Open the body. Real KEY=value lines (or a readable /.git/config) mean a leak.
Is a public .env.example OK?
Yes if it only has placeholder names and fake values. Never put real secrets in examples.
What if only public keys were in the file? Publishable Stripe keys and Supabase anon/publishable keys are designed to be public, but review what else was beside them. If anything secret shared the file, rotate the secrets.
Does deleting .env from the server fix a key that was already downloaded?
No. Anyone who fetched it still has the values. Rotate first, then remove the file.
Could VITE_ values still be in my JS after I fix /.env?
Yes. Fixing the file does not rewrite old bundles. Move secrets server-side, rebuild without VITE_ on those names, and redeploy.
How do I monitor this after launch?
Re-check after every deploy. StackSiren's free scan covers the basic /.env / /.git probes on domains you control; use a $9 report or a Solo plan if you want that check as part of a wider pass.
Fix guides for this topic
Platform checklists: Lovable · Bolt · Vercel · Replit