TL;DR
- AI code generators embed live secrets directly in source files more often than you'd expect
- The pattern comes from training data full of tutorials where authors hardcoded values for simplicity
- The fix is a single env var lookup, but you need a scanner to catch it before it reaches git history
I was doing a code review last week when I spotted it. Buried three files deep in a side project: a real Stripe live key, hardcoded directly into the payment service. The AI had generated it that way, the developer hadn't noticed, and it had already been committed twice.
Not a test key. Not a placeholder. An actual live key that could drain an account.
This isn't rare. I've started checking for hardcoded secrets as a reflex whenever I see AI-generated backend code. It shows up in roughly a third of the projects I look at - Stripe keys, OpenAI API tokens, database passwords, JWT signing secrets. The AI fills in the slot where it thinks a value goes, and the training data is full of tutorials where the author hardcoded their own key for "simplicity."
The Pattern (CWE-798)
The vulnerable version typically looks like this:
// ai-generated payment service
const stripe = require('stripe');
const client = stripe('sk_live_4xKp9mZqR3vNhJ8wXcTbL7eY');
const createCharge = async (amount, token) => {
return client.charges.create({
amount,
currency: 'usd',
source: token,
});
};
The AI grabbed a plausible-looking key format and stuck it inline. Sometimes it's a clearly fake placeholder, but sometimes it looks suspiciously real - or it's an actual key if the developer had pasted it anywhere in the conversation context.
Same pattern shows up with database connection strings:
const pool = new Pool({
connectionString: 'postgresql://admin:hunter2@db.myapp.io:5432/production'
});
And with JWT signing:
const token = jwt.sign(payload, 'my-super-secret-jwt-key-12345');
Why AI Does This
The model has seen thousands of tutorials, README files, and Stack Overflow answers where the author hardcoded a value. Stack Overflow in particular is full of examples from 2015 where someone wrote const apiKey = 'sk_test_...' because they were demonstrating a concept, not shipping production code.
The AI learned to fill the slot. It doesn't know the difference between a tutorial showing how an API works and production code that should read from environment variables. It predicts the most statistically likely token for that position, and the most likely token is a hardcoded value because that's what it saw most often.
The 2023 GitGuardian State of Secrets Sprawl report found over 10 million secrets committed to public GitHub repos that year. A meaningful chunk of those are almost certainly from developers shipping AI-generated code without reviewing it.
The Fix
It's one line. Replace:
const client = stripe('sk_live_4xKp9mZqR3vNhJ8wXcTbL7eY');
With:
const client = stripe(process.env.STRIPE_SECRET_KEY);
Full setup takes five minutes:
# 1. Install dotenv if you're not already using it
npm install dotenv
# 2. Create a .env file and add it to .gitignore immediately
echo "STRIPE_SECRET_KEY=sk_live_..." >> .env
echo ".env" >> .gitignore
# 3. Load it in your entry point
require('dotenv').config();
For production, use your platform's secrets manager: AWS Secrets Manager, GCP Secret Manager, Doppler, or environment variables set directly in your hosting dashboard.
Adding a pre-commit hook that runs gitleaks before anything goes out is worth doing:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
That catches most secrets before they ever touch git history.
If a Secret Already Got Committed
Rotate it immediately. Don't just delete the file and commit again - the key is still in git history. You need to:
- Invalidate the key in the provider's dashboard right now
- Generate a new one
- Use BFG Repo Cleaner or
git filter-branchto scrub the history if the repo is public or shared
Removing the value in the latest commit doesn't make it safe. History is permanent unless you actively rewrite it.
Catching It Before It Ships
I've been running SafeWeave on my projects for this. Its Gitleaks-based secrets scanner hooks into Cursor and Claude Code as an MCP server and flags hardcoded secrets before I finish the session. That said, even a basic pre-commit hook with gitleaks will catch most of what's in this post. The important thing is catching it before it reaches git history, whatever tool you use.
Top comments (0)