Skip to content
StackSiren

Guide

Exposed .env file, .git folder or VITE_ secret? How to fix it

Why VITE_ and NEXT_PUBLIC_ variables end up in the browser, how to check your own site for a public .env file or .git folder, and how to fix a leak.

Three ways secrets leak from AI-built apps

A .env file is served as a normal file. If it ends up in the deployed folder (for example inside a Vite public/ directory, or because a server serves the whole project folder), anyone can download it from yourdomain.com/.env.

A .git folder is served. If a server publishes the project folder including .git, people can often rebuild your source code and full commit history from it, including secrets that were committed and later deleted.

A secret gets a public prefix. Build tools copy variables that start with VITE_ or NEXT_PUBLIC_ straight into the JavaScript every visitor downloads. AI builders sometimes add the prefix to make an error go away, which turns a server secret into a public one.

Is a VITE_ variable secret? No

Vite's documentation says variables prefixed with VITE_ are exposed in client-side code and should not contain sensitive information such as API keys, because their values are bundled into your code at build time. Next.js does the same with NEXT_PUBLIC_: the value is inlined into the JavaScript sent to the browser during next build.

Other frameworks work the same way with their own prefixes, for example REACT_APP_ (Create React App), EXPO_PUBLIC_ (Expo) and PUBLIC_ (Astro and SvelteKit). Treat any of these as published.

The value is frozen at build time. Changing or deleting the variable in your host's dashboard doesn't remove it from deployments that were already built, so rotate the key as well.

Which keys can be public and which can't

Designed to be public: the Supabase publishable key (sb_publishable_...) or the legacy anon key, a Stripe publishable key (pk_...) and Firebase web config. A Supabase public key is only safe if Row Level Security is enabled on every table, because anyone can use it to call your database API.

Never give these a public prefix: a Supabase secret key (sb_secret_...) or the legacy service_role key, which bypass Row Level Security completely; OpenAI and Anthropic API keys; Stripe secret and restricted keys (sk_..., rk_...); AWS access keys; GitHub tokens; database connection strings; and email or SMS provider keys.

Supabase is replacing the legacy anon and service_role keys, which are long JWTs starting with eyJ, with publishable and secret keys. If you still have a legacy key in your front end, check that its role is anon and not service_role.

How to check your own deployed site

Open yourdomain.com/.env and yourdomain.com/.git/config in a browser, or run curl -i against both. A 404, a 403 or a redirect is fine. If you see lines such as KEY=value, or a [core] section in the Git config, the file is public. Only check sites you own.

Many single-page-app hosts return your home page with status 200 for any unknown path. That isn't a leak. What matters is whether the response contains the file's contents.

For public prefixes, open the browser developer tools, load your app, click through the main pages, then search the loaded JavaScript for sk_, sb_secret_, service_role and the names of your keys. Code that only loads on some pages may not show up until you visit them.

Check your repository as well: git ls-files shows whether a .env file is tracked, and git log --all -- .env shows whether one was ever committed.

Fix 1: rotate every leaked key first

Revoke or rotate the key in the provider's dashboard before anything else. Deleting it from code or history doesn't help if someone has already copied it, and old builds and clones may still contain it.

For Supabase, create a new secret key, switch every server component to it, then delete the old secret key or deactivate the legacy keys. GitHub's guide on removing sensitive data makes the same point: once a secret is rotated it can't be used, and that may be enough on its own.

Fix 2: move secrets server-side

Move every call that needs a secret into code that runs on a server: a Supabase Edge Function, a Netlify Function, or a Next.js route handler or server action. Store the key as a server-only environment variable with no public prefix, and have the front end call your function instead of the provider.

Lovable: keep API keys in Secrets, which are injected into server code and never reach the browser. Lovable's docs say VITE_ values are build-time, browser-exposed values, so they are only for public settings.

Bolt: add keys under the database panel's Secrets tab for server functions. If you export a Bolt project to Vercel or Netlify, the Secrets values aren't included, so recreate them as server-side environment variables on the new host.

Fix 3: stop .env and .git being deployed

Add .env and .env.* to .gitignore, and commit a .env.example with placeholder values instead. Never put a .env file inside Vite's public/ folder, because everything in public/ is copied as-is to the root of your build.

Deploy only the build output (such as dist or .next), not the whole project folder.

Vercel: when you deploy with the Vercel CLI it skips .git, .env.local and .env.*.local by default, but not a plain .env file. Add other files to .vercelignore. Netlify: secret scanning searches your repository code and build output, and fails the build if it finds a value from an environment variable marked as secret. Smart detection, on supported plans, also looks for likely secrets without that setup.

Self-hosted servers: block requests for dotfiles at the web server. On nginx, a rule such as location ~ /\.(?!well-known) { deny all; } blocks /.env and /.git while still allowing /.well-known/, which certificate renewals need.

Fix 4: clean your git history if needed

A .env file that was committed once stays in the history even after you delete it. GitHub recommends git-filter-repo (version 2.47 or later) with the --sensitive-data-removal, --invert-paths and --path .env options, followed by a force push. BFG Repo-Cleaner is an older alternative.

Rewriting history is disruptive: commit hashes change, collaborators need fresh clones, and forks keep the old data. Rotate the key first, then decide whether a rewrite is worth it. GitHub push protection can stop new secrets being pushed.

What StackSiren checks (and doesn't)

StackSiren makes one request each for /.env and /.git/config on your domain. It flags the .env file only if the response isn't an HTML page and contains several KEY=value lines, and the values are never shown. It flags the .git folder if /.git/config can be read.

It also scans your page HTML and the same-origin JavaScript files loaded by the page you submit for known secret formats, including Stripe, OpenAI, Anthropic, AWS, GitHub, Slack and SendGrid keys, Supabase secret and service_role keys and private keys. Matches are masked.

It doesn't check other file names such as .env.local or .env.production, files in subfolders, code that only loads on other pages, key formats it doesn't recognise, or your repository and its history. It matches the key itself, not the variable name, so a secret in a VITE_ variable is only caught if its format is recognised.

Builder-specific checklists

Check your app now

Free passive scan, results in under a minute.

Run a free scan