Every single person who has vibe coded has been here. It is not a sign that you are bad at this, and it is not a sign that the model is bad. It is a structural property of what happens when you ask a system to debug something it cannot observe.
Why the loop happens
The model cannot run your app. Sit with that, because everything follows from it. It cannot click the button, watch the network tab, or see what is actually in the database. It has your description of the symptom and a slice of the code, and from those two things it produces the most statistically plausible fix.
When the real cause is inside its blind spot, the most plausible fix is not the correct one. But the model has no way to know that, so it commits fully. It rewrites the file with total confidence. Now you have the original bug and an unnecessary rewrite. The next error is a little further from the truth than the last one, and each iteration pushes you further out.
The five ways out
1. Stop. Actually stop.
The strongest instinct in the loop is to send one more message, and it is exactly wrong. The next message is drawn from the same context that produced the last four failures, so it will fail in the same way. Close the chat. The loop is sustained by momentum and nothing else.
2. Go back to the last thing that worked
This is what the commits were for. git stash or git reset --hard to your last known-good commit and you have deleted the entire mess in one command, including the four speculative rewrites you have lost track of.
People resist this because it feels like throwing away work. It is throwing away broken work, and it is the single highest-value move available to you. Forty minutes of confused output is not an asset you are protecting. It is the problem.
git stash # park the mess, keep it retrievable
git log --oneline -10 # find the last commit where it worked
git diff HEAD # or: see exactly what changed since thenIf you have not been committing, this chapter is where that decision finally sends the invoice.
3. Make the model explain before it edits
The single most effective anti-loop prompt, and it works because it forbids the behaviour that creates the loop:
Do not change any code yet.
Explain what is actually happening, step by step, from the moment I click
the button to the moment the error appears. Name the specific file and line
where your explanation says it goes wrong.
If you are not sure, say which of the possible causes you cannot rule out
and tell me what to check to distinguish between them.That last paragraph is doing the heavy lifting. It gives the model a legitimate way to express uncertainty. Without it, the training pressure is to be helpful and decisive, so it guesses, and states the guess as fact. Give it an exit and it will often tell you, accurately, that it cannot see the thing it needs to see.
4. Get evidence it does not have
The loop is an information problem. So go get information. Print the thing. Log the actual value at the actual moment.
- What is really in the variable?
console.logit, right before the line that fails. Half of all doom loops end here. The value isundefined, and has been the whole time, three functions upstream of where you were both looking. - Is the request even happening? Open the browser Network tab and click the button. No request means the bug is in the frontend and every server-side fix was wasted. A request with a 401 means it is auth, not logic.
- Is the row actually there? Open the database and look. Not the UI, the table. “It saved successfully” and “the row exists” are different claims, and vibe-coded apps are notorious for the first without the second.
Then paste what you found. A model given the real value, the real status code, and the real row is a different and dramatically better collaborator than one given “it does not work.”
5. Delete it and describe it again
If a feature has been rewritten five times and is still wrong, the feature is not the problem. The description is. Delete the code entirely, go back to the spec, and write it properly this time, in a fresh conversation, with the constraints stated up front.
This is almost always faster than continuing. A fresh model with a good spec takes ninety seconds. A poisoned thread arguing with itself takes the rest of the evening and does not converge.
The rule that prevents most loops
And watch specifically for fixes that work by removing a check. A model under pressure to make an error stop will delete the validation that raised it, loosen the type, wrap the whole thing in a try/catch that swallows the exception, or comment out the permission test. The error genuinely does go away. The app is now silently broken in a way that no longer tells you it is broken, which is the exact shape of half the findings in the failure atlas.
// Before: this threw "user is not defined" and was, in fact, correct to.
if (!user) throw new Error("Not authenticated");
return await db.getPrivateNotes(user.id);
// After the model "fixed" it:
try {
return await db.getPrivateNotes(user?.id);
} catch {
return await db.getAllNotes(); // ships everyone's notes to a stranger
}The error is gone. The auth check is also gone. The app looks fixed and now serves data to anyone who asks.
That is not a strawman. It is a compressed version of a real pattern we find in shipped applications, and it is the reason a clean error log is not evidence of a clean app.