How to Find Exposed Secrets in Your Code Before They Ship
Leaked keys follow recognizable formats and hide in predictable places · frontend bundles, git history, and misprefixed .env files. Here is how to find them before they ship, and what to do when you find one.
By Daniel A · Kraftwire Software
· 7 min readKey Takeaway
Leaked credentials follow predictable patterns · most live in your frontend bundle, your git history, or a .env file with the wrong prefix, and most match recognizable formats you can search for in minutes. Run a manual grep pass for the obvious prefixes, then let the secret scanner and the .env file linter catch what your eyes skip over. And when you do find one, rotate the key first · deleting the code does not un-leak anything.
What Actually Counts as a Secret
A secret is any string that grants access on its own. If someone can paste it into a request and get data, money, or compute back, it is a secret, no matter how innocent the variable name looks. That covers:
- API keys for third-party services · Stripe, OpenAI, AWS, SendGrid, Twilio
- Database connection strings that embed a username and password
- Private signing keys · JWT secrets, PEM files, webhook signing secrets
- OAuth client secrets and refresh tokens
- Service role keys that bypass row-level security
Just as important is what does not count. Some identifiers are designed to ship to the browser: a Stripe publishable key (pk_live_), a Google Maps browser key locked to your domain, a Supabase anon key paired with real RLS policies. Treating those as radioactive wastes time you should spend on the strings that actually open doors. The skill is telling the two apart on sight.
The Formats You Can Recognize in a Code Review
Most providers prefix their keys deliberately, partly so scanners can find them. Learn these seven patterns and you can spot a leak in a pull request without any tooling:
AKIA · the first four characters of an AWS access key ID, which travels with a 40-character secret access key
sk_live_ · a Stripe live-mode secret key · its sandbox twin is sk_test_
sk- · an OpenAI API key · newer project-scoped keys start with sk-proj-
ghp_ · a classic GitHub personal access token · fine-grained tokens start with github_pat_
eyJ · the base64url encoding of {", which means the string is a JWT · long-lived service tokens often travel in this shape
-----BEGIN PRIVATE KEY----- · a PEM block, which is a private key sitting in plain text
postgres://user:password@host:5432/db · a connection string with credentials baked into the URL
The eyJ case deserves a second look before you panic. A JWT is readable by anyone, but not every JWT is a secret · an anon key that is designed to be public is different from a service_role key that bypasses every access rule. Decode it, check the role claim, then decide.
Where Secrets Actually Hide
Frontend bundles
Everything that passes through your build tool ships to every visitor. Vite inlines any variable prefixed VITE_, Next.js does the same with NEXT_PUBLIC_, Create React App with REACT_APP_. Prefix a server key that way and it lands, fully readable, in your production JavaScript · minification obscures nothing. Open your deployed site, view source, and search the bundle for sk_live_. A surprising number of shipped apps fail that ten-second test.
Git history
Deleting a key from the code does not delete it from history. git log -p still shows it, and bots scrape public repos for fresh credentials within minutes of a push. If a key was ever committed, treat it as leaked even if the file is long gone. This is the single most common way keys end up in the wrong hands, and fixing exposed API keys walks through the full recovery when it happens to you.
.env files with the wrong prefix
The .env file fails in two directions. First, the file itself gets committed because nobody added it to .gitignore before the first push. Second, and sneakier, a server-only secret gets a client prefix · VITE_STRIPE_SECRET_KEY looks tidy in the file and is a full leak in the bundle. The .env file linter flags both problems, plus placeholder values and duplicate keys, before they ship. For how the variables should be organized in the first place, see environment variables security.
Test fixtures and forgotten corners
Real keys get pasted "temporarily" into integration tests, seed scripts, docker-compose.yml, committed Postman collections, and .env.example files that were supposed to hold placeholders. Nobody audits these files, which is exactly why scanners find so much in them.
The Manual Grep Pass
Before any tooling, run the cheap check. From your project root:
Then check what your users actually receive by scanning the built output:
And check history, not just the working tree:
Any hit in any of the three is a finding. A hit in the second or third is an incident.
Manual vs Automated Scanning
The grep pass is fast and worth doing, but it only finds what you thought to search for. It misses providers you forgot, keys without famous prefixes, high-entropy strings assigned to innocuous names like config.token, and secrets split across lines or wrapped in template strings. Automated scanners layer two techniques on top of your prefix list: a much larger catalog of known provider formats, and entropy analysis that flags any string too random to be a word. A dedicated scanner runs those checks in seconds · paste a file, a diff, or a suspicious chunk of bundle output and it tells you what it recognizes. The right workflow is both: grep for the patterns you know while you code, scan before you ship.
Rotate First · Then Clean Up
The moment a secret has been exposed, assume it is compromised. Scrubbing the repo without rotating the key is theater · clones, forks, CI caches, and scraper databases all keep copies. The order of operations matters:
- Rotate or revoke the credential in the provider dashboard. This is the only step that actually ends the exposure.
- Check the provider's usage logs for activity you do not recognize, starting from the earliest possible leak date.
- Move the new key to a server-side environment variable with no client prefix, and confirm nothing in the frontend reads it.
- Clean the repo if the key was committed ·
git filter-repo rewrites history, but understand this is hygiene, not remediation.
- Add guardrails so it cannot recur ·
.gitignore the env files, add a pre-commit scan, and make the check part of code review.
Skipping straight to step 4 is the classic mistake. A rewritten history with an unrotated key is still a breach waiting on someone else's schedule.
Check Your Code Before It Ships
Make secret detection a habit that costs you two minutes per release instead of a weekend per incident. Paste anything suspicious into the secret scanner, run every environment file through the .env file linter before deploy, and keep the grep one-liners in your shell history. When you want the whole picture · leaked keys, exposed endpoints, missing headers, and speed in one report · run a free security scan and see what your app is showing the world.