Skip to content
Back to field notes
SecretsJune 6, 20266 min read

Hardcoded secrets in AI-generated code: what to grep before you deploy

AI coding tools often inline a literal key into source to make a feature work. This note shows the grep patterns and history scans to run on your own repo before you deploy.

SecretsGitPre-deploy

The key the model pasted into your source

The concrete risk is direct: there is a real, live credential sitting as a literal string inside a file in your repository, and it will ship with your next deploy. An AI coding tool, asked to wire up a third-party service, often inlines the key straight into the code to make the call work on the first try instead of reading it from an environment variable. That string then lives in your source tree, gets committed, and rides into the build. Anyone with the repo, or anyone who can read the deployed artifact, can read the key.

This is the upstream version of a problem you can also catch in the browser. Our notes on VITE_ environment variables exposed in the client bundle and on finding an API key in the browser network tab cover the deployed surface: what an outside visitor can download. This note is the check you run earlier, on your own repository, before anything deploys at all. The two are complementary, not the same check. A key can be hardcoded in a server file that never reaches the browser and still be a serious leak the moment the repo is shared or the history is pushed.

  • Observable in your own repo: a literal credential string in a tracked file, or in the commit history even if it was later deleted.
  • Distinct from the bundle check: this finds the key before the build exists, including secrets in server-side files that a browser scan would never see.
  • This is exposure of secret material (CWE-798, use of hard-coded credentials), not a broken login. The key is valid; the mistake is its location.

Why AI tools inline keys instead of wiring env vars

The split between "read this from an environment variable" and "put the value here" is one of the easiest things for a generation step to collapse. An AI builder optimizes for a feature that runs. When the prompt is "connect to Stripe" or "call the OpenAI API," the shortest path to a working call is a literal key in the code, because that needs no env file, no loader, no deploy-time wiring. The call succeeds, the UI lights up, and the build looks finished.

A few patterns put a literal credential into source without anyone deciding to:

The review step, where someone asks whether this value should be a literal at all, is the part that does not happen automatically. So the check has to be something you run on purpose.

  • A key pasted directly into a client or fetch call so the example runs, then never moved out.
  • A config or constants file the tool generates with the real value filled in, rather than a placeholder pointing at an env var.
  • A key that was hardcoded during a quick fix, committed, then "removed" in a later edit, which leaves it in history while the working tree looks clean.

Grep your repo for high-signal secret patterns

You can find most hardcoded keys without any tooling, using git grep against your own repo. Run these from the repository root. Each one is a plain text search for a known credential prefix or a generic name that often sits next to a value.

Search the working tree first. Run git grep with the -n flag (to show line numbers) for the high-signal provider prefixes, one pattern at a time or combined with -e:

For each hit, open the file and decide: is this a literal value, or a reference to an env var (something like process.env.STRIPE_KEY, which is fine)? A literal string after the equals sign is the finding.

  • sk_live and sk_test for Stripe secret keys; pk_live and pk_test are publishable and usually fine, but worth eyeballing.
  • sk-ant- for Anthropic API keys, and sk-proj- and sk- for OpenAI project and user keys.
  • AIza for Google API keys (Maps, Cloud, and others share this prefix).
  • service_role and eyJ for a Supabase service key and the JWT prefix it starts with.
  • AKIA for AWS access key IDs, and aws_secret_access_key for the matching secret.
  • xoxb- and xoxp- for Slack bot and user tokens.
  • ghp_ and github_pat_ for GitHub personal access tokens.
  • Then the generic names that flag a hardcoded value even when the prefix is unfamiliar: api_key, apikey, secret, password, token, private_key, and BEGIN PRIVATE KEY for inlined PEM material.
  • Suspicious result: a real credential appears as a literal string in a tracked file, for example a sk_live_ value assigned directly in source.
  • Safer result: the matches are env-var references, placeholders, test fixtures clearly marked as fake, or publishable/anon keys that are designed to be public.

Why a clean working tree is not enough: scan the history

Deleting a key from the current files does not remove it from the repository. Git keeps every committed version. A secret that was hardcoded in one commit and "removed" in the next is still sitting in the history, fully readable to anyone who clones the repo or browses it on a host that exposes history. If that repo is or ever becomes public, the deleted key is just as exposed as one in the working tree.

This is exactly the case AI-assisted, fast-iteration workflows produce: a key gets inlined to make something work, committed, then refactored out a few commits later once the env var is wired. The diff looks clean; the history is not.

Two named tools scan properly for this, and both can read history rather than just the current files:

Run one of these against the full history, not just the current checkout. A hit in an old commit is a real leak. If you find one, removing the key from history is a separate, heavier operation (rewriting history and force-pushing), and it still does not un-expose a key that was already pushed somewhere public. The credential itself has to be treated as compromised.

  • gitleaks: runs a ruleset of secret patterns. gitleaks detect scans the full git history by default, and gitleaks dir (or the protect mode) scans the working tree or staged changes.
  • trufflehog: scans for secrets and can verify many of them against the provider. trufflehog git on a local path or repo URL walks the commit history, not only HEAD.

Fix it: rotate first, relocate, then add a pre-commit scan

There are three moves, and the order matters because rotation is the part people skip.

  • Rotate anything you found, because it is compromised. A secret that has been committed has to be assumed leaked, the same as one that shipped in a bundle. Revoke and reissue it at the provider before you do anything else. Moving a leaked key without rotating leaves the old value valid.
  • Relocate the value to an environment variable or a secret store, and read it from there in code. The literal string leaves the source entirely; the file references the variable name instead. On the host, set the real value in the deploy platform's environment configuration, never back into a committed file.
  • Add a pre-commit secret scan so the next inlined key is caught before it ever lands in history. gitleaks ships a pre-commit hook; running it on staged changes blocks a commit that introduces a known secret pattern. This closes the loop the AI tool leaves open.

Rotating and relocating stops the key from being valid and stops the next commit from carrying a new one. Neither removes the old value from history that is already pushed. Once a secret has been committed to a repo that anyone else can reach, you cannot know who cloned it, so the only safe assumption is that it is burned. Rotate first; clean up history second if you must, but treat the credential as already public.

Where VibeCodeGuard fits, and what a clean grep does not prove

VibeCodeGuard's Launch Check works on the deployed surface, not your repo. It fetches your public URL and scans the served JavaScript for secret-shaped strings, flagging anything that matches a known credential pattern in the client bundle, with severity and fix direction. That catches a hardcoded key that made it all the way into the build and shipped to the browser. The repo grep and history scan in this note catch keys upstream, before deploy, including secrets in server-side files that never reach the bundle. They are complementary: the grep is your pre-deploy gate, the Launch Check is the check on what actually went live.

A clean grep is not proof of safety. Pattern matching only finds the shapes you searched for. A custom or internal credential format, a key split across lines or stored base64-encoded, or a token with no recognizable prefix can slip every pattern above and every default ruleset. The grep and the scanners reduce the obvious cases; they do not certify the repo is secret-free.

A clean grep and a clean history scan mean no known-shaped secret turned up, not that your code holds no secrets safely. Treat any literal credential you find as a launch blocker and a rotation trigger; treat a clean result as one signal, not a guarantee. Next step: grep your repo and its full history today, rotate and relocate anything you find, add a pre-commit secret scan, then run a focused Launch Check against the public URL before you share it.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.