Exposed secrets in your client bundle: how to find and fix them
About 1 in 4 vibe-coded apps we scan has at least one credential leaked in the client bundle — usually a Stripe key, an OpenAI token, or a Supabase service-role key. The AI didn't mean to leak it; it just didn't know which keys are safe to expose. Here's how to find yours and fix it before someone else does.
What 'in the client bundle' means
When your app runs in a user's browser, everything the browser receives is public. The JavaScript bundle. The CSS. Every HTTP response. Anyone can open DevTools and read it. So any string that ends up in that bundle — even an environment variable, even a comment, even a forgotten console.log — is fully exposed to the world.
Every service has two kinds of keys. The PUBLISHABLE / PUBLIC key (pk_..., NEXT_PUBLIC_*, prefixed VITE_PUBLIC_*) is safe to ship to the client — that's its job. The SECRET key (sk_..., service_role, root token) must NEVER touch the client bundle. If it does, you have a problem.
The keys most commonly leaked by vibe-coded apps
- Stripe — sk_live_... and sk_test_... secret keys. Anyone with sk_live_ can charge cards on your account, refund anything, read all customer data.
- OpenAI / Anthropic — sk-... and sk-ant-... API keys. Anyone with one can run unlimited inference on your bill.
- Supabase — service_role keys. Bypasses all row-level security; anyone has admin access to your database.
- AWS — AKIA... + secret access keys. Full account access, including provisioning resources or exfiltrating data.
- Sentry — auth tokens with project:write scope. Lets attackers tamper with your error reporting.
- Resend / SendGrid / Postmark — API keys with email-send permission. Spam from your domain reputation.
- Firebase — admin SDK keys (NOT the web config). Bypasses Firebase security rules entirely.
- Twilio — Account SID + auth token. Attacker can send SMS / make calls on your bill.
How they get there
Most leaks come from three patterns:
- Wrong env-var prefix. Next.js exposes anything prefixed NEXT_PUBLIC_ to the client. Vite exposes anything prefixed VITE_. Vibe-coding tools sometimes default to the public prefix because it 'just works' — and the secret tags along.
- Direct hard-coding. AI assistants occasionally inline a key as a literal string in the code, especially during refactors or when fixing a bug. 'Move this constant to the top' moves the secret into the bundle.
- Console.log debugging. Leaving console.log(process.env) in a client component prints every env variable in the browser console.
How to check your own bundle
Method 1 — DevTools network search
- Open your deployed site in Chrome.
- DevTools → Sources tab → Cmd-Shift-F (Mac) or Ctrl-Shift-F (Windows) to search all loaded files.
- Search for: sk_live_ sk_test_ sk- service_role SUPABASE_SERVICE AKIA AIza ya29. -----BEGIN
- If anything matches in a .js or .map file, you have a leak.
Method 2 — curl + grep
For sites with many bundles, scripted check is faster:
- curl your-site.com/_next/static/chunks/*.js | grep -E 'sk_(live|test)_|sk-[a-zA-Z]|service_role|AKIA|AIza' — Next.js example
- Repeat for Vite (/assets/*.js), Remix (/build/*.js), Astro (/_astro/*.js), or whatever your framework uses.
Method 3 — Comply Code scan
Paste your URL into Comply Code. We fetch every JS bundle the page loads and grep against a corpus of ~40 known secret-prefix patterns (Stripe, OpenAI, AWS, Supabase, Anthropic, etc.). Free, no signup. The same scanner we use ships in the public Comply Code product because secret leaks were one of the four pillars of risk we initially designed around.
What to do if you find a leaked secret
Speed matters. The longer a secret is exposed, the higher the chance it's been scraped — there are automated bots that scan public sites and GitHub repos for keys 24/7.
- Rotate the key NOW. Don't fix the code first; rotate first. Stripe / OpenAI / AWS / Supabase all have one-click rotation in their dashboards. The old key stops working immediately.
- Generate the new key and store it in the server-only environment (Vercel env vars, Fly secrets, etc.) — never in NEXT_PUBLIC_ or VITE_.
- Refactor the code to use the new key via a server-side route (an API route, a server component, a serverless function). The client never sees the secret.
- Re-deploy.
- Re-scan the deployed site to confirm the secret is gone from the new bundle.
- Audit usage for the period the key was exposed. Stripe: check Logs for charges or refunds. OpenAI: check Usage for inference. AWS: check CloudTrail.
Preventing it on the next deploy
- Adopt the env-var naming rule: PUBLIC variables get NEXT_PUBLIC_ / VITE_PUBLIC_; secrets do NOT. Make this a team norm.
- Pre-commit hook to grep for known secret patterns (Stripe sk_live, OpenAI sk-, AWS AKIA). Husky + a small grep script is enough.
- Server-side proxy pattern. The client never calls Stripe / OpenAI directly. It calls your /api/stripe/create-charge, which holds the secret server-side. This is the architectural prevention; it eliminates the leak risk entirely.
- CI step that scans your built bundle for known prefixes and fails the build if any are found. trufflehog, gitleaks, or a hand-rolled grep all work.
- Re-scan post-deploy. Comply Code's CI integration runs the same secret-detection check after every Vercel / Fly / Netlify deploy.
Special note: Supabase service-role keys
Supabase deserves its own paragraph. It ships two keys for every project: the anon key (safe to publish — that's its job) and the service_role key (must never touch the client). Vibe-coded apps sometimes use the service_role key because it 'just works' (it bypasses row-level security, so queries always succeed). The cost: anyone with the key reads or writes everything in your database.
Rotate immediately in your Supabase dashboard. Then audit auth.audit_log_entries and Storage's audit log for the period of exposure. If you can't tell whether data was exfiltrated, assume it was — and notify users per your privacy policy. GDPR Article 33 requires a breach notification within 72 hours of becoming aware.
Common questions.
Is the Supabase 'anon' key really safe to expose?
Yes, when you also have row-level security (RLS) configured correctly. The anon key has the same permissions as an unauthenticated user — which means whatever your RLS policies allow. Without RLS, the anon key has full database access. Always enable RLS on every table that holds user data.
I just rotated my Stripe key. How do I know if anyone used the old one?
Stripe Dashboard → Developers → Logs → filter by your old key prefix. You'll see every API call made with it. If you see charges, refunds, or customer reads from IPs you don't recognise, you've had unauthorised use. Stripe's support team will help you investigate and (in many cases) reverse fraudulent charges.
How long does it take for a scraper to find a leaked key?
For public GitHub repos, often within minutes — there are crawlers running continuously. For deployed websites, somewhat longer, but services like Shodan, GreyNoise, and various security-research crawlers do find them. Realistic answer: assume a leak older than 48 hours has been seen.
What if my AI assistant put the secret there?
Same response — rotate, audit, prevent. The cause doesn't change the urgency. But this is a good reason to write your AI-assistance rules with explicit guidance: 'Never include API keys directly in client-side code. Use server-side routes.' Many vibe-coding platforms (Cursor, Lovable, Bolt) now respect explicit rules files for this.
Should I add a .env to .gitignore?
Yes, always. But .gitignore doesn't help if you've already committed the file. If you've ever committed a .env, treat every key in it as compromised — rotate them all, then add to .gitignore. Tools like git-filter-repo and BFG Repo-Cleaner can scrub the history, but rotation is the only safe assumption.
Can Comply Code detect leaks before deploy?
Comply Code scans live URLs, not source repos. For pre-deploy scanning, use trufflehog (open source) or gitleaks (open source) — both run as pre-commit hooks. Combine the two: pre-commit catches leaks before they reach prod; Comply Code's deploy-side scan catches the cases where something slips through.