How keys end up in the browser
Anything your frontend can read, your visitors can read. When an AI editor wires up "call OpenAI from my app", the fastest code puts the key in a variable the browser bundle can see. Build tools make it easy to do by accident: in Vite, any variable prefixed VITE_ is inlined into the JavaScript; in Next.js it's NEXT_PUBLIC_; in Expo, EXPO_PUBLIC_. Rename OPENAI_API_KEY to VITE_OPENAI_API_KEY to "fix an undefined error" and the key ships to every visitor.
Minification does not hide it. Bots scrape public sites and GitHub for key patterns continuously, and a leaked OpenAI or Anthropic key can be drained within hours. A leaked Stripe live secret key is worse: it can issue refunds, read customer data and create charges.
What this checker looks for
- OpenAI (
sk-proj-…,sk-svcacct-…) and Anthropic (sk-ant-…) API keys - Stripe live secret and restricted keys (
sk_live_,rk_live_). Publishablepk_live_keys are meant to be public and aren't flagged. - AWS access key IDs, GitHub tokens, Slack tokens, SendGrid keys and PEM private key blocks
- Supabase service_role JWTs (decoded, so we only flag the dangerous role)
- Google API keys, reported as informational: they are normal in Firebase and Maps configs but should be restricted.
We fetch your page and up to 25 same-site script bundles, the same files a browser downloads. Matches are masked in the results (first 8 and last 4 characters) and we never publish key values. Expect some limits: keys fetched at runtime from an API, or loaded from third-party domains, won't be seen, and pattern matching can occasionally misfire on random strings.
Found a key? Do this in order
- Revoke or rotate it first. Deleting it from your code does not help; the old bundle is cached on CDNs and in scrapers' databases. Go to the provider dashboard and create a new key.
- Check usage and billing for unexpected calls since the key was deployed. Set spending limits where the provider supports them.
- Move the call server-side. Create an API route, serverless function, or Supabase Edge Function that holds the key in an environment variable without a public prefix, and have your frontend call that route instead.
- Protect the new route. A proxy that forwards any prompt to OpenAI for anyone is still an open wallet. Require a signed-in user and add rate limits.
- Purge the history. If the key was committed to a public repo, rotating it is the fix; rewriting git history is optional cleanup.
- Re-scan to confirm the new build is clean.
Prevent it next time
Treat public env prefixes as "this will be printed on a billboard". Keep a short list of what may be public (Supabase anon key, Firebase web config, Stripe publishable key, analytics IDs) and keep everything else server-only. Add a secret scanner like gitleaks or GitHub push protection to your repo. Before launch, run grep -rE "sk-|sk_live_|service_role" dist/ on your built output. And if you'd rather have it automated, ShipSniffer's full scan and source-code scan check your built site and your repo together.