Make the bad state impossible, not unlikely
A few days ago I was watching a returning visitor get deflected by something I built to help them. The site has a guided tour, a little scripted walk where the assistant shows you around the terminal and the rest of the build. Someone came back, typed an actual question, and the bot answered with a stalling line instead of the answer. It was not broken in the way a crash is broken. It was doing exactly what I told it to do. That is usually worse.
The tour had grown two enforcement modes. There was a lock mode, where after you finished the scripted beats the assistant stopped having a normal conversation and started deflecting, and a silenced mode that kicked in past a message count. Both made sense the day I wrote them. The tour was supposed to be the on-ramp, and lock mode kept a visitor from wandering off the rails before the payoff. Then real traffic hit it. A returning vibe coder does not want the on-ramp again. They want the answer. And there was a second, quieter problem underneath: a flag in storage, set by an older version of the build, could resume a tour the visitor never started. A stale value from last week was deciding what someone saw today.
I started where everyone starts, which is patching the deflection. Add a condition so returning visitors skip lock mode. Add another so the stale flag gets ignored when the version does not match. Each patch worked and each patch made the thing it was patching a little more permanent. I was hardening a shape that should not have existed. The bad states were not a bug in the logic. They were a bug in the type.
So I deleted them. The tour's mode lived in a small union, the set of values the mode could legally hold, and two of those values were lock and silenced. I removed both from the union and made the tour opt-in behind a single command instead of something that armed itself and then had to be contained. The moment those two values were gone, the compiler turned into a migration checklist. It walked me through every place in the code that had assumed a visitor could be in lock or silenced mode and refused to build until each one was handled. The deflection branches in two route handlers, the scroll lock keyed off the tour phase, the storage reads, the persona instructions that told the model to stall. I did not have to remember where the bad states leaked. The type system enumerated them for me and would not let me ship until the list was empty.
That is the whole move, and it generalizes well past one tour. It is one of the habits that separates vibe coding that holds from a build that just accretes patches. When a feature keeps getting into a state you do not want, you have two options. You can defend against the state, which means another conditional, another patch, another thing to remember. Or you can make the state unrepresentable, which means the bad value cannot be constructed in the first place, and the compiler becomes the thing that remembers for you. The first option makes the bug less likely. The second makes it impossible. A vibe coder reaching for the first option by reflex is the most common version of this I see, because patching feels like progress and deleting feels like loss. It is backwards. The patch adds surface area. The deletion removes it.
I versioned the storage key while I was in there, so a flag written by the old build carries an old name the new code never reads. Same principle, one layer down: the bad input is not validated against, it is structurally unreachable. Then I ran it through a thirteen-scenario live gate, the boring part that earns the confidence. Fresh visitor, returning visitor, a seeded legacy profile carrying the exact stale flag that caused the original mess, a hammer that drove the message count past every old threshold. Nothing deflected, because nothing could. There is no code path left that produces a deflection, so there is nothing to test for except that the absence holds.
The thing I keep relearning is that a gate has to earn its keep. Lock mode was a guardrail I added to be safe, and it spent its whole life making the experience worse for the people I most wanted to keep. Not every check is protection. Some checks are just ceremony that degrades the thing and hides stale state nobody is watching. The strongest guardrail is not the one you defend with another conditional. It is the one the type system makes impossible to violate, so the vibe coded feature simply cannot enter the state you were afraid of. Removing the tour ceremony had a second payoff I did not plan for: the heavy 3D hero it leaned on came out in the same pass, and the homepage shed about 883 kB of runtime JavaScript that used to load for nothing.
If you are formalizing how you build with AI and you keep finding yourself patching the same shape over and over, that is usually the signal to delete instead of defend, and it is exactly the kind of thing worth working through on purpose. Work with VibeKoded if you want a sparring partner on turning a tangle of conditionals into a shape that cannot go wrong.
The bug that taught me this never crashed anything. It just quietly did the wrong thing to the right people, which is the failure mode that survives longest because nothing alerts on it. The fix was not cleverness. It was subtraction, and then lett