You shipped it. The demo looked great. Your AI assistant wrote 90% of the code, you tweaked a few things, pushed to production, and... it fell over within a week.
I've been there. Last month I inherited a codebase that was almost entirely AI-generated. The original developer had prompted their way through the entire thing — API routes, database schema, frontend components, the works. It ran fine locally. In production, it leaked memory, had three SQL injection vectors, and the auth flow could be bypassed by deleting a cookie.
This isn't an anti-AI rant. I use AI coding tools every single day. But there's a difference between using AI as a power tool and using it as a replacement for understanding what you're building. Let me walk you through how to identify the common failure patterns in vibe-coded projects and build a workflow that actually holds up.
The Root Cause: Prompting Without a Mental Model
The core issue isn't that AI writes bad code. It often writes perfectly reasonable code — for a different context than yours. When you prompt "build me a REST API with user auth," the model gives you something plausible. But it doesn't know:
- Your actual threat model
- Your deployment environment
- Your scale requirements
- Which edge cases your users will hit
The generated code fills in those blanks with assumptions. And assumptions are where bugs hide.
Step 1: Audit the Critical Paths First
When you've got a vibe-coded project that needs fixing, don't try to review everything at once. Start with the paths where bugs cause real damage.
# Quick script to find routes that handle sensitive operations
# Run this against an Express/FastAPI/Django project
import ast
import sys
import os
SENSITIVE_PATTERNS = [
'password', 'token', 'secret', 'auth',
'payment', 'delete', 'admin', 'session'
]
def scan_file(filepath):
with open(filepath, 'r') as f:
content = f.read()
hits = []
for i, line in enumerate(content.split('\n'), 1):
lower_line = line.lower()
for pattern in SENSITIVE_PATTERNS:
if pattern in lower_line:
hits.append((i, line.strip(), pattern))
return hits
# Walk through your project and flag sensitive code paths
for root, dirs, files in os.walk(sys.argv[1]):
dirs[:] = [d for d in dirs if d not in ('node_modules', '.venv', '__pycache__')]
for f in files:
if f.endswith(('.py', '.js', '.ts')):
path = os.path.join(root, f)
results = scan_file(path)
if results:
print(f"\n--- {path} ---")
for line_num, line, pattern in results:
print(f" L{line_num} [{pattern}]: {line}")This won't catch everything, but it gives you a prioritized hit list. Every result is a place where the AI made assumptions you need to verify.
Step 2: Check for the Classic AI Code Smells
After reviewing dozens of AI-generated codebases, I've noticed patterns that show up constantly:
- Optimistic error handling. The AI wraps things in try/catch but the catch block just logs and continues. In production, this means silent failures that corrupt state.
- Copy-paste architecture. The model generates similar-but-slightly-different code for each endpoint instead of abstracting shared logic. This means bug fixes need to happen in twelve places.
- Missing input validation. The AI trusts incoming data because the prompt didn't mention adversarial users.
- Hardcoded assumptions. Connection strings, timeouts, retry counts — all baked in as literals because that's what the prompt context implied.
Here's a quick grep pattern to find some of these:
# Find empty or log-only catch blocks (JS/TS)
grep -rn "catch" --include="*.ts" --include="*.js" -A 2 src/ | \
grep -B 1 "console.log\|console.error\|// TODO"
# Find hardcoded connection strings or secrets
grep -rn "localhost:\|127.0.0.1\|password.*=.*[\"']" \
--include="*.ts" --include="*.js" --include="*.py" src/
# Find routes without any validation or middleware
grep -rn "app.post\|app.put\|app.delete\|router.post" \
--include="*.ts" --include="*.js" src/ | \
grep -v "validate\|middleware\|auth\|guard"Step 3: Add the Safety Net the AI Forgot
The biggest thing missing from most vibe-coded projects? Tests. Not because testing is glamorous, but because nobody prompts "now write comprehensive tests for all the edge cases you just glossed over."
Here's my approach: write tests for the boundaries, not the happy path.
# Instead of testing that login works with valid credentials,
# test what happens at the edges
def test_login_with_sql_injection_attempt():
response = client.post("/auth/login", json={
"email": "admin'--",
"password": "irrelevant"
})
assert response.status_code == 422 # should reject, not 500
def test_login_with_missing_fields():
response = client.post("/auth/login", json={})
assert response.status_code == 422
assert "email" in response.json()["detail"][0]["loc"]
def test_login_rate_limiting():
# AI-generated auth almost never includes rate limiting
for _ in range(20):
client.post("/auth/login", json={
"email": "test@test.com",
"password": "wrong"
})
response = client.post("/auth/login", json={
"email": "test@test.com",
"password": "wrong"
})
assert response.status_code == 429 # rate limited
def test_token_expiry_is_enforced():
# Generate a token, manually set its exp to the past,
# verify it gets rejected
expired_token = create_test_token(exp_minutes=-5)
response = client.get("/api/me", headers={
"Authorization": f"Bearer {expired_token}"
})
assert response.status_code == 401These tests catch the exact class of bugs that AI-generated code tends to ship with.
Step 4: Build a Workflow That Actually Works
Here's the workflow I've settled on after a year of using AI tools daily:
semgrep, bandit (Python), or eslint-plugin-security (JS) catch the low-hanging vulnerabilities that AI happily introduces. Add them to your CI pipeline.Prevention: The 30-Second Rule
Here's a rule I use now: after the AI generates any block of code, I spend at least 30 seconds thinking about what it got wrong. Not reading the code — specifically looking for what's missing or what assumptions it made.
Sounds small. But that habit catches probably 80% of the issues I described above. The AI gives you a login endpoint? Thirty seconds: "Where's the rate limiting? What happens with malformed JSON? Is the password comparison timing-safe?"
The Honest Truth
AI coding tools are genuinely powerful. I write code faster with them, and the code is often fine. But "often fine" and "production-ready" are different things. The gap between them is understanding — understanding your system, your users, and your failure modes.
Vibe coding isn't a sin. Shipping code you don't understand is. The fix isn't to stop using AI — it's to stop letting it think for you.
