How AI Assistants Leak Secrets Into Your Codebase
AI coding assistants frequently generate hardcoded API keys, tokens, and passwords. Learn the exact patterns and how to detect them before they reach production.
The Credential Leak You Did Not Know You Shipped
Russia's GRU recently harvested Microsoft Office authentication tokens from over 18,000 networks by exploiting misconfigured routers. No malware required. The tokens were simply sitting there, poorly protected.
The parallel to AI-assisted development is uncomfortable but direct: LLMs generate code with hardcoded credentials more often than most developers realize, and those credentials sit quietly in your repo until someone decides to look.
This is not a theoretical risk. It is a systematic pattern baked into how large language models learn to write code.
Why LLMs Generate Hardcoded Secrets
Language models are trained on billions of lines of public code, including the bad parts. GitHub, Stack Overflow, and tutorial blogs are full of examples where an API key is pasted inline because it was the fastest way to make the example work. The model learns that pattern. When you ask it to "add a call to the Stripe API," it has seen thousands of examples where the key lived directly in the function body.
The model is not being careless. It is doing exactly what its training data taught it.
The Three Patterns to Watch
Pattern 1: Inline initialization
# What an AI assistant often generates
client = stripe.StripeClient("sk_live_abc123xyz")
# What it should generate
client = stripe.StripeClient(os.environ["STRIPE_SECRET_KEY"])
Pattern 2: Default fallback values
This one is subtler and easier to miss in review:
// AI-generated with a "helpful" fallback
const apiKey = process.env.OPENAI_API_KEY || "sk-proj-real-key-here";
// The correct version
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) throw new Error("OPENAI_API_KEY is not set");
The fallback pattern is particularly dangerous because it passes CI/CD pipelines in environments where the env var is set, but exposes the hardcoded key when the variable is absent, such as in a developer's local clone or a misconfigured staging environment.
Pattern 3: Test fixtures with real-looking values
AI assistants sometimes generate test files with values that look like real credentials:
# Generated test fixture
def test_payment():
token = "tok_visa_4242424242424242" # looks fake, but...
charge = create_charge(token, api_key="sk_test_BQokikJOvBiI2HlWgH4olfQ2")
Some of these "test" keys are real Stripe test-mode keys that can be used to enumerate account data.
Catching These Before They Ship
Static analysis tools like gitleaks, trufflehog, and detect-secrets can catch most literal secrets at commit time. Configure them as pre-commit hooks:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
But static tools have a blind spot: they catch secrets by pattern, not by context. A secret rotated into an environment variable but then logged to stdout or written to a temp file will slip through.
The more reliable approach is scanning the generated diff before committing, particularly looking at any file that an AI assistant touched. Treat AI-generated code the same way you would treat code from a new contractor who has not yet been briefed on your secrets management policy.
Key Takeaways
- LLMs generate hardcoded credentials because their training data includes millions of examples of that pattern from public repositories and tutorials
- The fallback pattern (
process.env.KEY || "hardcoded-value") is the most dangerous variant because it passes automated checks in most environments
- Run a secrets scanner as a pre-commit hook on every AI-assisted project, and treat the AI's output with the same skepticism you would apply to any untrusted external code