Anatomy of a Vibe-Coded SaaS: Eleven Things You Can Grep For Before Lunch

Each check below is a fingerprint. The first six show how the project was run: whether anyone reviewed anything, and where the agent put what it didn't understand. They're smells, not bugs. The last five find damage: code that works on a laptop and breaks in production, or leaks something it shouldn't.
Run them from the repository root. Swap src for wherever your code lives, and add --exclude-dir=node_modules if library code floods the output.
1. fix, then actual fix
git log -100 --format=%s | grep -ciE '^(fix|update|wip|asdf)This counts commit messages that say nothing. Our rule of thumb: more than 20 out of the last 100 means nobody was reviewing. Commits were save points, not decisions.
2. A README that's mostly emojis
Not a grep. Open it. A star next to every feature, and a "Why I built this" section longer than the install steps. The codebase is a personal blog with extra steps.
3. useState where a reducer belongs
grep -rc "useState(" src --include="*.tsx" | sort -t: -k2 -n | tail
This lists the files with the most state hooks. Vibe-coded React tends to grow parallel pieces of state that should be one reducer. A component with a dozen of them is where the next bug lives.
4. A helpers file that ate the project
find src \( -name "*util*" -o -name "*helper*" \) -type f | xargs wc -l | sort -n | tail
Anything past 2,000 lines is where the agent dumped what it didn't know where to put. Look for duplicates while you're in there: three versions of formatDate(), two slugify() helpers.
5. // TODO: fix later next to a real bug
grep -rnE "TODO|FIXME" src | wc -l
Each one is a decision someone put off. Agents leave them when they aren't sure. Founders rarely come back.
6. Two ORMs
grep -oE '"(@prisma/client|drizzle-orm|typeorm|sequelize|mongoose|kysely)"' package.json
More than one line of output means two ways to talk to the same database. Somebody asked "what's the best ORM" in two sessions and kept both answers, each with its own migrations.
This counts commit messages that say nothing. Our rule of thumb: more than 20 out of the last 100 means nobody was reviewing. Commits were save points, not decisions.
2. A README that's mostly emojis
Not a grep. Open it. A star next to every feature, and a "Why I built this" section longer than the install steps. The codebase is a personal blog with extra steps.
3. useState where a reducer belongs
__CODE_BLOCK_1__
This lists the files with the most state hooks. Vibe-coded React tends to grow parallel pieces of state that should be one reducer. A component with a dozen of them is where the next bug lives.
4. A helpers file that ate the project
__CODE_BLOCK_2__
Anything past 2,000 lines is where the agent dumped what it didn't know where to put. Look for duplicates while you're in there: three versions of formatDate(), two slugify() helpers.
5. // TODO: fix later next to a real bug
__CODE_BLOCK_3__
Each one is a decision someone put off. Agents leave them when they aren't sure. Founders rarely come back.
6. Two ORMs
__CODE_BLOCK_4__
More than one line of output means two ways to talk to the same database. Somebody asked "what's the best ORM" in two sessions and kept both answers, each with its own migrations.
Turn the eleven checks into commands for your repo
Tell ChatGPT your stack and folder layout. It will rewrite each check as a command for your repo and say what counts as a red flag.
“|”
7. Hardcoded localhost
grep -rn "localhost:[0-9]" srcA fetch("http://localhost:3000/api/...") works on your laptop, where something is listening on port 3000. Deployed to Vercel, nothing is, and the call fails. Often silently, which brings us to check 9.
8. NEXT_PUBLIC_STRIPE_SECRET_KEY
grep -rnE "(NEXT_PUBLIC|VITE)_[A-Z_]*(SECRET|SERVICE_ROLE|PRIVATE)" . --exclude-dir=node_modulesNext.js ships every NEXT_PUBLIC_ variable to the browser, and Vite does the same with VITE_. Anyone who opens dev tools can read a secret under either prefix. A leaked Stripe secret key lets a stranger read your customer list and issue refunds as you. If this returns anything, rotate the key today.
9. Empty catch
grep -rnE 'catch *(\([^)]*\))? *\{ *\}|\.catch\(\(\) *=> *\{ *\}\)' srccatch (e) {} swallows the error and moves on. Agents write it when the prompt was "make it work". It works until the day it quietly doesn't, and the logs say nothing.
10. A migration named add_ that drops a column
grep -rli "drop column" supabase/migrations prisma/migrations drizzle migrations 2>/dev/null | grep -i addRename a field in a Prisma or Drizzle schema and the generated migration can come out as a drop plus an add. Development data is fake, so nobody notices. In production, the old column's data is gone. Read every file this returns.
11. Zero tests, one console.log("here")
find . -name node_modules -prune -o \( -name "*.test.*" -o -name "*.spec.*" \) -print | wc -l
grep -rn "console.log" src | wc -lA zero on the first line and a big number on the second is a debug session that shipped. When we took over Oria, a platform built fast with AI coding agents, there were no tests at all, and secrets were hardcoded in the code.
What to fix first
Almost everything here is fixable. Most of it is also fixable in the wrong order.
Lock down secrets first, then authorization, then the data layer. Tests come after that, and new AI features last. Founders tend to run it backwards and ship a new feature while the leaked key is still live.
Ran the eleven checks and didn't like what came back?
We audit and fix AI-built codebases at a flat price. Rescue audit and plan from $500: a written audit in 48 hours and a flat-price plan for the fix. Send your email and we'll set up a 30-minute call within 24 hours.
How to read your results
- Hits on checks 1 to 6 only. The project was built fast and nobody tidied up. Fixable, not urgent.
- Any hit on check 8. Stop and rotate the key. Then carry on.
- Two or more hits among checks 7, 9, 10 and 11. Production is less stable than it looks, and swallowed errors hide it. That's where a second pair of eyes pays for itself.
The check grep can't do
Eleven greps can't tell you whether one customer can read another customer's data. For that, log in as users in two organizations and try. It was the finding that mattered most at Oria, and no grep would have caught it.
Related reading
- We inherited an AI-generated codebase. Here is what we found: the six findings from a real rebuild, including the one grep can't see
- We don't reject AI. We use it like surgeons: how we keep these fingerprints out of code we write with agents
- From Enrichlead to Moltbook: a short tour of the vibe-coded graveyard: public failures of vibe-coded products
Built with a specific tool? Cursor, Lovable, Bolt, Claude Code. The wider picture: vibe-coded app rescue. The service: Vibe Code Rescue.
Enjoyed this article? Share it with others
Related Posts

Your AI-Built Supabase Is One Curl Away From a Leak. Eight Checks for Tonight.
The key in your front end is public by design. What protects your data is row-level security, and an AI-built app may not have it. Eight checks for tonight, starting with the curl an attacker would run.

The 30-Minute Vibe-Coded App Audit: Run It Yourself First
Twenty-five yes-or-no questions about your repo, logins, data, payments, secrets and error tracking. Most need a dashboard, not a terminal. Thirty minutes and one score tell you whether you need help.

What a $500 Code Audit Report Looks Like (Sample Inside)
Before you pay $500 for an audit, you should see what comes back. Here's a sample built from a real AI-built codebase, details changed: six findings with severity, a keep, fix or replace verdict for each, and a plan with one fixed price per phase.