Skip to content
Rotwise
Findings6 min read

The three findings we see in almost every vibe-coded app

N+1 queries, swallowed exceptions and hardcoded secrets show up in nearly every AI-assisted codebase we scan. Here is why the tools produce them, and how to fix each one for good.

We have now scanned enough AI-assisted codebases to notice a pattern. The stacks differ, the products differ, the teams differ, and yet three findings show up in almost every report. Not because the models are careless, but because a prompt ends exactly where these problems begin: the code works in the demo, and nobody asked what happens after.

1. The N+1 query

Ask an assistant to "show each order with the customer's name" and you will get something like this:

orders.each do |order|
  puts "#{order.number} — #{order.customer.name}"
end

It is correct. It is also one query for the orders and one more query per order for the customer. With ten orders in development nobody notices. With ten thousand in production the page takes twelve seconds and the database is the first thing to fall over.

The assistant answered the question it was asked. The data-access shape was never part of the question, and it is invisible in a snippet. The fix is one line: load the association up front.

orders.includes(:customer).each do |order|
  puts "#{order.number} — #{order.customer.name}"
end

In JavaScript with Prisma the same mistake looks like a findMany followed by a findUnique inside a loop, and the fix is an include on the outer query. Rotwise flags a query inside a loop as a performance finding, usually P1 or P2, and batches every occurrence in a file together so the review is one screen long.

2. The swallowed exception

This one is quieter and costs more:

begin
  PaymentGateway.charge(order)
rescue => error
  nil
end

In Python it is except: pass. In TypeScript it is an empty catch (e) {}. The assistant is optimizing for "the code runs", and a rescue that returns nothing makes any failing call disappear. The order is marked paid, the charge never happened, and the bug surfaces three services away in a support ticket.

The repair is not to remove error handling, it is to make it honest. Rescue the narrowest class you can name, log with enough context to find the record again, and either recover on purpose or let the failure be visible.

begin
  PaymentGateway.charge(order)
rescue PaymentGateway::CardDeclined => error
  order.mark_declined!(reason: error.message)
end

Anything you did not name still raises, which is what you want. Rotwise treats a bare rescue with an empty body as a correctness finding and proposes a scoped rescue with logging, leaving the recovery decision to you.

3. The hardcoded secret

STRIPE_KEY = "sk_live_51H..."

It usually arrives during a "just make it work" session: a key pasted from a dashboard so the demo runs, then committed along with everything else. The assistant has no notion of your secret store, so it happily keeps the constant where it found it and reuses it in the next file.

Moving the key to an environment variable or encrypted credentials is the easy half. The hard half is remembering that git history is forever, so the key has to be rotated, and that a scanner should be part of CI so the next one never lands. Rotwise runs a secret scanner plus pattern rules on every scan and treats a live key as a P0 finding, the first thing in the first batch.

What ties them together

All three are correct in the demo and wrong in production. They pass the only test the assistant had, which was "does it run", and they fail the tests nobody wrote. That is the shape of most AI-generated debt: not broken code, but code that was never asked the second question.

The remedy is boring and it works. Measure the codebase regularly, rank what you find by severity, and fix it in batches small enough to review over coffee. The three findings above are usually gone after the first two pull requests.

← All posts