When we scanned 24 AI-built applications, credentials sitting in client-side code came up again and again. The cause was consistent: something needed real data for the preview to work, the key went where the browser could reach it, and nobody moved it afterwards.
The baseline
Secrets in environment variables, .env in .gitignore, and a committed .env.example listing the variable names with no values.
That's the minimum. It works and it's better than most codebases manage.
Check what actually shipped
The config is not the deployed artefact. On a frontend build, anything the bundler could see may be in the bundle.
The specific trap on Next.js is the NEXT_PUBLIC_ prefix, which inlines a variable into the client bundle at build time. It's documented and it still catches people, because the same variable name works in development where everything runs on the server.
Open your deployed site, view source, and search the JavaScript for sk_, secret, key and token. That's the only check that tells you the truth.
Stop it at commit time
A pre-commit hook catches this before it becomes permanent. gitleaks and trufflehog both do the job and run in a second or two.
Add the same check to CI, because hooks are local and someone will commit from a machine that doesn't have them.
If it's already committed
Rotate the secret. That's the fix, and everything else is secondary.
Rewriting history with git filter-repo or BFG removes it from your copy. It doesn't remove it from clones, forks, CI caches, or anywhere that has already fetched. Assume anything committed to a repository, including a private one. Is compromised, and treat the rewrite as tidying rather than remediation.
Providers make this easy. Stripe, AWS and GitHub all support issuing a new key and revoking the old one in a couple of minutes.
Secret managers
Environment variables stop scaling once you have several services, several environments, and a rotation policy.
AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Doppler and similar give you central storage, access control, audit logs and rotation. The application fetches at startup rather than reading from the environment.
Worth it when you have more than a handful of secrets or a compliance requirement to evidence rotation. Not worth it for a single service with three keys.
CI and containers
CI systems have their own secret storage, use it, and confirm masking is on so secrets don't appear in logs.
In containers, prefer runtime injection over build arguments. A build arg is baked into the image layer and readable by anyone who pulls it.
What scanners catch
This is the class rule-based scanning is genuinely good at, and it's worth being specific about why. A long high-entropy string with a recognisable prefix is a pattern in the literal sense, so a matcher does the job. In our RealVuln benchmark, hardcoded secrets is where rule-based tools stay closest to the LLM-based ones, and it's the clearest example of the general rule: pattern matching works when the bug has a shape.
Which makes this one of the cheaper problems on the list. A secret scanner in CI plus a pre-commit hook covers most of it, and both are free. There's no good argument for paying for this one.