A GET request to /.env should not return a file
The failure here is blunt. If your deployed app serves a request for /.env and answers with the file contents, then every secret in that file is now public. No login, no exploit, no clever trafficking of edge cases. A browser, a crawler, or a one-line script asks for the path, and the server hands back the plaintext.
What lives in a typical .env makes this severe. The file usually holds the database connection URL with its password, third-party API keys, a JWT signing secret, SMTP credentials, and sometimes a payment provider's secret key. Any one of those reaching the public surface is enough to call the leak a launch blocker. All of them in one file is the common case.
This is an exposure of a deployment artifact, not a code bug. The application logic can be perfectly correct. The problem is that a file which was only ever meant for the build and runtime is sitting inside the directory the web server is configured to serve, so the server treats it like any other static asset.
- A 200 response with the file body means the secrets are readable by anyone who requests the path.
- The same pattern applies to common variants: /.env.production, /.env.local, /.env.development, and a stray .env.bak or .env.save.
- Reachability is the whole problem. You do not need anyone to prove they read the file; if it answers, treat the secrets as already gone.
Why AI-built and quick-deploy apps hit this
The root cause is almost always that the project root and the document root are the same directory. When the folder you committed your code into is the folder the web server serves, every dotfile in that folder ships too, including .env.
AI-generated deploy configs and fast static-host setups land on this default often. A generated Dockerfile or host config that copies the whole repository into the served directory, or a static host pointed at the project root instead of a build output folder, will publish .env right alongside index.html. The app runs, the demo works, and nothing in the happy path reveals that a dotfile is now web-accessible.
- Root-as-webroot: the served directory is the same one that holds .env, so the file is reachable by its path.
- Copying the whole repo into the served directory during deploy, instead of only the built assets.
- Missing dotfile deny rules: many default server configs do not block requests for files starting with a dot, so .env is served like any other file.
- The file works for the app at runtime, which hides that it is also answering public requests. The app reading its own .env tells you nothing about who else can.
Check your own public URL
You can confirm this on your own deployment in under a minute, and it is a safe self-check because you are only requesting your own files. Do this against your own URL, never someone else's.
From a browser or a command line, request each of these paths against your production origin and look at the response: /.env, /.env.production, /.env.local. A request like curl -i https://your-app.example.com/.env shows you the status code and the body in one shot. Watch the status code and the first lines of the body.
Check each variant separately. A 404 on /.env does not tell you what /.env.production or /.env.local return, because they are different paths and may have been copied differently.
- Suspicious result: a 200 status with body content that looks like KEY=value lines, for example DATABASE_URL=, JWT_SECRET=, or SMTP_PASSWORD=. That is the file, and the secrets are exposed.
- Safer result: a 404 (not found) or 403 (forbidden), with no env content in the body. A redirect to a login or app page is also fine, as long as the env values never appear.
- Watch for soft failures: some single-page apps return a 200 with the app's HTML for every unknown path. That is not a leak by itself. The leak is when the body is the env file's KEY=value lines, not your index page.
Get secrets out of the webroot, then rotate
The fix has two halves, and both matter. Closing the path is not enough on its own.
First, stop shipping the file and stop serving dotfiles. Move secrets to the host's runtime environment variables or a managed secret store, so the values are injected at runtime and the .env file never has to live in the deployed directory. Keep .env out of the served folder entirely, point your host at the build output rather than the project root, and add a server rule that denies any request for paths beginning with a dot. Each of these is a separate layer, and you want more than one.
Second, rotate every secret that was reachable. This is the part that gets skipped, and it is not optional. If /.env answered a request at any point, you have to assume the values are compromised, because a public file may have been scraped before you noticed. Treating the exposure as fixed by only blocking the path leaves live, leaked credentials in production.
- Move database URLs, API keys, the JWT secret, and SMTP credentials into runtime env vars or a secret manager; keep .env out of the deployed directory.
- Add a deny rule for dotfiles in the server or host config, and confirm it by re-requesting /.env and seeing a 404 or 403.
- Rotate database passwords, API keys, the JWT signing secret, and SMTP credentials. A rotated secret is the only one you can trust after exposure.
Closing the route and rotating the secrets are two different jobs. Denying /.env stops future requests from reading the file. It does nothing about copies already taken while the path was open. Rotation is what actually invalidates a leaked secret, so do both, in that order, and do not call it fixed after only the first.
Where VibeCodeGuard fits, and what it does not prove
VibeCodeGuard's Launch Check requests /.env, /.env.production, and /.env.local on your public surface and flags any 200 response as a launch blocker. It tells you, from the outside, whether these paths are reachable on the URL you are about to share, which is the exact thing an anonymous visitor or crawler would find.
What it checks is reachability, not history. The scan cannot tell you whether the file was already scraped while it was open, and it does not read your secret store or confirm that you rotated anything. So a flag means the path is live and the secrets are reachable; a clean result means these specific paths did not answer on this scan, not that no secret ever leaked.
Next step: request /.env, /.env.production, and /.env.local against your own production URL right now. If any returns a 200 with file contents, treat it as a blocker, get the secrets out of the webroot, rotate them, and only then run a focused Launch Check before sharing the URL publicly.
Assume any reachable secret is already burned. If the scan flags /.env, blocking the path is only half the fix; the secrets that were behind it have to be rotated before you treat the issue as closed. A clean Launch Check is one signal that the public surface is quiet, not a guarantee that a secret was never exposed earlier.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.