Five searches that tell you what you are shipping
A manual check you can run on any deployed site with nothing but a browser — five searches, what each one finds, and how to tell a real finding from a false positive.
This is the whole method, given away. Five searches against your own deployed JavaScript, in the order that finds the most in the least time, plus the harder half that nobody writes about: how to read the results without scaring yourself over a chunk hash.
Start by collecting the files. View source on your deployed site and note the script URLs, or let the shell do it.
BASE=https://example-app.test
curl -s "$BASE" | grep -oE '/_next/static/[^"]+\.js' | sort -u > chunks.txt
wc -l chunks.txt
The searches
1. eyJ — every JSON Web Token
while read -r c; do curl -s "$BASE$c"; done < chunks.txt \
| grep -oE 'eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' | sort -u
- Finds: Supabase anon and service-role keys in the legacy format, Auth0 tokens, anything else JWT-shaped.
- Why first: it is the highest-value search on this list, because the same three-segment shape covers both the credential that belongs in a browser and the one that is a full database compromise.
- Next step: decode every hit. It takes one command and it is the only way to tell them apart.
2. Provider secret prefixes
while read -r c; do curl -s "$BASE$c"; done < chunks.txt \
| grep -oE '(sk_live_|sk_test_|rk_live_|whsec_|sk-proj-|sk-admin-|sk-svcacct-|sk-ant-api)[A-Za-z0-9_-]{8}' | sort -u
- Finds: Stripe secret and restricted keys, webhook signing secrets, OpenAI and Anthropic keys.
- Why it matters: unlike a JWT there is nothing to decode and nothing to interpret. The prefix is the whole claim, and every one of these is a finding.
- Watch for:
sk_test_is lower severity thansk_live_because no real money moves, but it still exposes your test data. And a baresk_prefix is issued by several providers, not only Stripe.
3. AKIA and ASIA — AWS
while read -r c; do curl -s "$BASE$c"; done < chunks.txt \
| grep -oE '(AKIA|ASIA)[0-9A-Z]{16}' | sort -u
- Finds: AWS access key IDs.
AKIAis long-lived;ASIAis a temporary STS credential. - Note: the access key ID alone is not enough to authenticate. Search near any hit for a forty-character base64 string, which would be the matching secret — the pair is what matters.
- Do not search for forty base64 characters on its own. See the false-positive section below.
4. Connection-string schemes
while read -r c; do curl -s "$BASE$c"; done < chunks.txt \
| grep -oE '(postgres|postgresql|mongodb(\+srv)?)://[^\s"'"'"']{8,}' | sort -u
- Finds: database URLs containing a username, a password, a host and a port in one field.
- Why it happens: almost always an import boundary rather than carelessness — a shared config module or a barrel export that pulled a server-only value into client code.
- Severity: treat any hit as critical. This is direct, unmediated database access with no policy layer in front of it.
5. PEM armour
while read -r c; do curl -s "$BASE$c"; done < chunks.txt | grep -c 'BEGIN.*PRIVATE KEY'
- Finds: RSA, EC, DSA, OpenSSH and PGP private keys, and the service-account keys that arrive inside JSON credential files.
- How it happens: somebody imported the whole credentials file rather than a field from it.
- Frequency: rare, and unambiguous when present. There is no benign reason for a private key to be in a browser bundle.
Reading the results
Obviously fine
Some hits are supposed to be there, and treating them as incidents is how you learn to ignore your own checks. A Supabase anon or publishable key, a Stripe pk_live_, a Firebase AIza config, a Sentry DSN, a Mapbox pk. token — all designed for the browser, all constrained by something other than secrecy.
For the Firebase case in particular: Google publishes one of these in its own documentation. If a scanner reports it as a leak, the scanner is wrong.
Obviously not fine
Anything from searches 2, 4 and 5. Plus any JWT whose payload decodes to a role claim of service_role:
echo '<token>' | cut -d. -f2 | tr '_-' '/+' | base64 -d
If you find one of these, stop reading and rotate it at the provider. Removing it from the next build does nothing about the copies already served, and cached or archived chunks can keep serving the old file after you redeploy.
The ambiguous middle, and the search not to run
The tempting sixth search is "find me anything that looks random". Do not add it. Minified JavaScript is full of high-entropy strings that are not credentials: webpack chunk hashes, content digests, base64-encoded assets, subresource integrity attributes, UUIDs, and the minifier's own generated identifiers. A forty-character base64 match is the shape of an AWS secret key and also the shape of half a dozen entirely mundane things.
Entropy on its own is not evidence. What makes a finding is a known prefix, a decodable structure, or corroborating context nearby — a matching access key ID, or an AWS-specific variable name three characters away. A string that is merely random tells you nothing, and a check that reports it will bury the four searches above in noise.
If a hit does not fall cleanly into "designed to be public" or "matches a known secret format", the useful question is not how random it looks. It is what happens if you paste it into that provider's API and see whether it authenticates — against your own account, on a test call.
What this does not cover
Preview and branch deployments, which are public, built with the same environment, and never checked. Third-party scripts injected at runtime, which are not in chunks.txt because they are not yours. Source maps, which reconstruct your original code including the comments. And tomorrow's build, which is a different artifact — every deploy is a new one, and today's clean result says nothing about it.
Running this by hand once is worth an afternoon. That last point is the reason it became a product.