Almost nobody loses a key to an attacker. They publish it themselves, in a commit, a screenshot, or a bundle of JavaScript sent to every visitor. The people who find it are not watching you personally. They run scanners across public repositories continuously, which is also why GitHub scans public code for known key formats and notifies the provider that issued them.

The idea

A secret is any string that grants access on its own: an , a database password, a , a webhook signing secret. Because it works for whoever holds it, the only meaningful question after an exposure is who has held it, and the only reliable fix is to make the old value stop working.

code
  where secrets end up in public

  your machine          published surface        who reads it
  ------------          -----------------        ------------
  .env  ----(committed)---> git history  ------> automated scanners
  chat  ----(pasted)------> public issue -------> search engines
  code  ----(NEXT_PUBLIC_)--> browser bundle ----> anyone who opens devtools
  screen --(recorded)-----> demo video --------> anyone who pauses the frame

How it works

  • Committed to git. A .env file gets added because .gitignore was missing or wrong. Deleting it in a later commit does not help: git keeps every earlier version, so the key stays readable in the history and in every clone that already pulled it. GitHub's guidance on removing sensitive data says to treat the credential as compromised and rotate it, because rewriting history cannot reach copies other people hold.
  • Pasted into a public place. A stack trace with the connection string in it, dropped into a GitHub issue while asking for help.
  • Shipped to the browser. Frontend code runs on the visitor's machine, so anything compiled into it is public. In Next.js, any environment variable named with the NEXT_PUBLIC_ prefix is inlined into the JavaScript bundle by design, and minification does not hide it.
  • Filmed. A terminal or an editor tab visible in a demo video. Frames can be paused.

Not every key is a secret. Both Stripe and Supabase issue you two kinds. The publishable key (pk_ at Stripe, sb_publishable_ at Supabase) is built to sit in a browser and is safe there. The secret key (sk_, sb_secret_) is not, and Supabase's in particular skips every database rule you wrote. Older Supabase projects show this pair under the earlier names anon and service_role, which are being retired; the split is the same one.

What to do

Step 1 is for the day something is exposed. Steps 2 to 4 are for today, whether or not anything has happened. Run all commands from the root of your repository.

  1. If a secret is out, rotate before you investigate. Issue a new key in the provider's dashboard, put it everywhere the old one lived, redeploy so the running server picks it up, then revoke the old one. Investigation takes hours and rotation takes minutes, and until the old value is dead the exposure is still live. Some secrets, such as a webhook signing secret, cannot have two live values at once, so those mean a short outage rather than a clean swap.
  2. Ask two separate questions about your secrets file. Is it tracked, and is it ignored. Run git ls-files .env.local first: if it prints the filename, git is tracking it and the value should be treated as public, because an ignore rule has no effect on a file git already tracks. Then run git check-ignore -v --no-index .env.local, which prints the matching rule when one exists and nothing when it does not. The --no-index matters: without it, the command also stays silent for a tracked file, which is the exact case you are hunting.
  3. Search for the value, not only the name. git grep -n "NEXT_PUBLIC_" lists what you have deliberately published, and every match should be something you would print on a billboard. That catches the labelled leak. For the unlabelled one, take the first ten characters of a real secret key and run git grep -n "sb_secret_" and git log -S"sk_live_" --oneline --all, which finds a key pasted straight into a file and one that was committed and later removed.
  4. Turn on push protection so a recognised key format is blocked at push time rather than found later. On GitHub it is under the repository's Settings, in Advanced Security, as Push protection under Secret Protection. It is free on public repositories; a private repository needs a paid plan, in which case steps 2 and 3 are what you keep doing by hand.

Where it breaks

Rotation has a cost worth knowing before the bad night: swapping a database password or a webhook secret takes production down until every place that uses it has the new value. Write that list of places down now, while nothing is on fire, because step 1 assumes you have it. Scanners only catch formats they recognise, so a self-issued token or a plain password can sit in a repository for years without an alert. And rotation limits future damage without undoing past damage. If a key with read access to your database was public for a week, the copy someone took still exists, which makes it a data exposure question as well as a key question. When it has already gone wrong is the page for that.