What vibe coding actually is, and where it breaks down
A working definition of the loop, the six places it reliably fails, and a one-sentence test for whether a task is safe to hand over.
You shipped a CSV importer on Wednesday that you never read. It worked on the sample file, the tests you asked for went green, and the following Tuesday it silently dropped every row where a customer name contained a quoted comma.
The gap between code that runs and code somebody understands is the whole subject here. Vibe coding is not an attitude or a seriousness level. It is a specific workflow with a specific failure surface, and knowing the shape of that surface is what lets you use it on purpose instead of drifting into it.
The workflow, stated plainly
The term was popularised by Andrej Karpathy in early 2025, describing letting the model write the code and largely not reading it. The discourse around the phrase has been louder than the idea, which is narrow and easy to state.
You describe intent in prose. The agent writes the code. You judge the result by running it rather than by reading the diff. If the behaviour matches what you asked for, you keep it. If it does not, you describe the gap and go round again.
Notice what is absent. Reading the diff is not a step. That single omission is what makes the loop fast, and it is the root of everything in the second half of this article.
Where it genuinely works
The loop works when running the thing tells you nearly everything you need to know.
Throwaway scripts qualify. A one-off migration you will run once and delete, a script that reorganises four hundred files on disk, a parser for a log format you will never see again. You can inspect the output directly and the blast radius ends when the process exits.
Prototypes qualify, as long as everyone involved agrees they are prototypes. Glue code between two APIs you already understand qualifies. So does UI scaffolding: a form, a table, a modal, a settings page. If it renders wrong, you can see that it rendered wrong.
Well-trodden stacks help enormously. An Express route, a React component, a Dockerfile for a Node service. The conventions are stable and heavily represented in whatever the model learned from. One-file tools qualify partly because the whole thing fits on a screen if you ever do need to read it.
One underrated case: a one-off in a language you do not know. Writing a sixty-line Go utility when you write TypeScript all day is exactly the trade you want, because your own reading of that diff would not have caught much anyway.
Where it breaks down
Six situations, all of them specific.
The codebase outgrows what the agent can hold at once. Early on, the relevant files fit comfortably in a session. Later they do not, and an agent that cannot see your existing formatCurrency writes a second one with slightly different rounding. This compounds quietly and is worth understanding on its own terms in The duplicate code problem.
Domain rules nobody ever wrote down. Invoices for one market round VAT per line, another rounds on the total. Refunds after ninety days need a manager flag. None of this is in the repo, so the agent produces something plausible and internally consistent that is simply not what the business does.
Money, auth, concurrency, data migration. These four share a property: plausible code passes casual testing and then fails under load, fails adversarially, or fails once and irreversibly. A permission check that is correct for the single-tenant case. A read-modify-write with no lock. A migration that is not idempotent, run twice by a retrying deploy.
// what the agent wrote: plausible, and wrong for our VAT rules
export function lineTotalNaive(unitPence: number, qty: number, vat: number) {
return Math.round(unitPence * qty * (1 + vat));
}
// what finance actually requires: VAT rounded per line, then added
export function lineTotal(unitPence: number, qty: number, vat: number) {
const net = unitPence * qty;
return net + Math.round(net * vat);
}
Both pass a test written by the same agent that wrote the code. They differ by a penny per line, which becomes a reconciliation ticket a month later.
When you cannot distinguish "works" from "appears to work". Cache invalidation, retry logic, timezone handling, anything with "eventual" in the name. If your only signal is that you clicked it and it looked fine, you have no signal.
Private or undocumented internal APIs. Your company's auth service with a wiki page that went stale three years ago. The agent guesses at the request shape. The guess is well-formed, confident, and wrong in a way that only shows up against production data.
Performance. An agent optimises for code that reads correctly, not code that runs quickly. A database call inside a loop over a thousand rows is behaviourally perfect and operationally awful, and nothing in the run-it-and-see loop surfaces that until the table gets big.
Velocity is front-loaded
This is the part people underrate, and it is economic rather than technical.
Days one to three are genuinely fast. The repo is small, everything relevant fits in one session, and you get an enormous amount of working software for very little typing. That speed is real. It is not a trick.
Week three is slow. Every change now requires understanding code that no human has read. Bugs take longer to locate because you have no mental model to search with. Changes ripple in directions you did not predict because you never learned the shape of the thing. The agent, meanwhile, can no longer see the whole project at once, so its edits are locally sensible and globally inconsistent.
Review was not skipped. It was deferred. Deferred review is paid back with interest, and the repayment schedule is set by your incidents, not by you: two in the morning, unfamiliar code, time pressure, someone asking for an ETA. Reviewing code you did not write is a learnable skill, but the cheaper move is to avoid accumulating the debt in the areas where it compounds fastest.
The one-sentence test
Here is the heuristic that has held up best: if you cannot state the acceptance test in one sentence, you are not ready to hand the task over.
Not because the agent cannot attempt it, but because you will have no way to judge what comes back. The loop depends entirely on your ability to tell a good result from a bad one by looking at behaviour. Remove that and you are not vibe coding, you are gambling and calling it velocity.
<!-- vague: nothing in here can be checked -->
Add pagination to the users list endpoint. Make it clean.
<!-- testable: names the file, the shape, the pattern, the check -->
In src/routes/users.ts, change GET /users to accept `limit` (default 25,
max 100) and `cursor` query params, returning `{ items, nextCursor }`.
Follow the cursor pattern already used in src/routes/invoices.ts.
Done when `pnpm vitest run src/routes/users.test.ts` passes, including a
new case asserting the second page shares no ids with the first.
The second version is not longer because prompts should be long. It is longer because the task is now falsifiable.
The rule of thumb: vibe code anything you would be happy to delete and regenerate from scratch, and read the diff on anything you would have to debug at three in the morning. Most work sits clearly on one side or the other, and the honest answer for the rest is that you already know which side it is on and are hoping otherwise.
Keep reading
Context windows explained for people who just want to ship
Why your agent gets worse an hour in, the signals that tell you it has happened, and the handful of habits that keep sessions sharp.
Why your agent forgets everything, and what to do about it
Three tiers of memory for coding agents, and why a lint rule enforces your standards better than any paragraph of markdown ever will.