TL;DR
- Vibe-coded apps ship fast and quietly bury hardcoded secrets, weak auth, and injectable queries in code that runs fine.
- You do not need a security team to catch them. One scan pass before you commit catches most of it.
- Build a scan-and-fix loop that runs while you code, not a security audit you dread doing after launch.
Last month I inherited a side project a friend built entirely by prompting Cursor. It worked. Signups, Stripe checkout, a dashboard, all of it, in a weekend. Then I opened the repo.
The first thing I searched for was the word "key". Fourteen results. Not in a config loader, not read from the environment. Hardcoded, right there in the source, committed to a public GitHub repo.
That is the tax nobody warns you about with vibe coding. The app runs, so you assume it is fine. But "runs" and "safe to ship" are two different claims, and the gap between them is exactly the stuff a language model was never optimized to get right.
The vulnerable code
Here is the pattern, lightly changed to protect the guilty:
// CWE-798: Use of Hard-coded Credentials
const stripe = require('stripe')('sk_live_51H8xR2eZvKYlo2CqQf...');
const supabase = createClient(
'https://xyzcompany.supabase.co',
'eyJhbGciOiJIUzI1NiI...service_role_key'
);
That service_role key bypasses every row-level security rule in the database. It was sitting in a client-adjacent file, and in git history, forever. Anyone who cloned the repo had full read and write on the production database.
Why this keeps happening
Ask an AI editor to "connect to Stripe" and it does the most direct thing: it puts the key where the code needs it. Tutorials do the same. Half of StackOverflow does the same. The model learned from millions of examples where the secret is inline, because inline is what makes a snippet copy-pasteable.
Nobody wrote "and now move this to an environment variable and rotate it if it ever leaks" inside the code sample. So the model hands you working code that is also a liability, and it does it with total confidence. There is no warning. The app runs.
The fix
The fix itself is boring, which is the point.
# 1. Find every secret already in the code and in git history
gitleaks detect --source . --verbose
# 2. Pull them out of source and read from the environment instead
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
# 3. Rotate anything that ever touched a commit. Assume it is burned.
# 4. Add a pre-commit hook so it never happens again
pip install pre-commit
pre-commit install # runs gitleaks + semgrep on every commit
The worry-free version of this is not "never make the mistake." You will. It is "the mistake gets caught the same minute you make it, before it reaches a commit." That is the whole trick. Scan while you build, fix it in place, move on to the next prompt.
I have been running SafeWeave for this. It hooks into Cursor and Claude Code as an MCP server and flags hardcoded secrets, injectable queries, and missing auth checks before I move on. That said, even a basic pre-commit hook with semgrep and gitleaks will catch most of what is in this post. The important thing is catching it early, whatever tool you use.
Top comments (0)