The bullshit of vibe coding

The bullshit of vibe coding

The login screen looks excellent. The dashboard has charts. There is a dark mode, obviously. You built it in an afternoon and the demo works. Then somebody asks whether one customer can download another customer's invoice. Suddenly, nobody is quite sure where to look.

That is my problem with vibe coding. We have made it remarkably easy to produce software that looks finished, and remarkably tempting to stop checking whether it is.

I am happy to let AI write code. I am not happy to pretend that code nobody understands has somehow become somebody else's responsibility.

A convincing facade

Imagine putting a beautiful new facade on a building with rotten floorboards, improvised wiring and a staircase that leads nowhere. From the street, it looks like a successful renovation. You could photograph it, sell the vision and get plenty of enthusiastic comments.

You still would not want people living in it.

An elegant white and blue facade propped against an unfinished building with exposed pipes, damaged floors and improvised supports.

Software has an awkward advantage over buildings: the floor does not have to collapse during the viewing. A permissions bug can sit quietly behind a beautiful interface. A payment can be duplicated only when a request is retried. A data migration can work perfectly against the three records used for the demo.

The screenshot will not show any of that. Yet the screenshot is increasingly what gets presented as evidence that the engineering has been done.

Polish is cheap evidence

A polished interface used to suggest that somebody had spent time on the product. It was never proof of quality, but it at least represented effort. Generative tools weaken even that signal: an attractive screen can arrive before anyone has decided what should happen when the operation fails.

There is nothing wrong with making a prototype attractive. The mistake is promoting visual completeness into technical confidence. The login button working does not tell you that the access model works. A success notification does not tell you that the data was saved correctly.

Who is making the decisions?

Using AI to implement a change you understand is a useful development workflow. What I am criticising here is the habit of prompting, accepting and prompting again while allowing your understanding of the system to fall further behind.

Ask for a customer portal and the assistant has to fill in missing decisions. How should data be fetched? Where should state live? What happens after a timeout? Which dependency should handle the form? If you have not supplied those answers, something still gets implemented.

That does not mean the decisions are necessarily bad. It means they are decisions, and somebody needs to evaluate them. A generated default does not become a product requirement because it arrived inside a working component.

The invoice that should not exist

Take a hypothetical invoice page. The UI only lists Alice's invoices. Its route guard requires a logged-in user. Clicking an invoice calls GET /api/invoices/123. The API checks that the caller is authenticated, loads invoice 123 and returns it.

Every visible part of the demo can work while the most important rule is missing: is this caller allowed to read this particular invoice?

Maybe the generated solution got that right. Maybe it did not. You cannot settle the question by asking whether the screen works. You have to inspect where authorisation is enforced and test a request for another customer's invoice. The business rule needs to survive somebody bypassing the interface entirely.

No amount of nicer spacing fixes a missing permission check.

One more prompt

The dangerous loop starts when each symptom produces another prompt, but never a better understanding of the system.

A request fails. Add a retry. The user can submit twice. Disable the button. The result still appears twice after a timeout. Add a flag. A different screen now behaves strangely. Ask the assistant to refactor the shared service.

Every step can make the specific thing you are looking at seem better. Meanwhile, the original question remains unanswered: what should happen if the server completed the operation but the browser never received the response?

A patch can move the problem

For an operation that charges money or creates an order, retry behaviour belongs in the design. Disabling a button helps with accidental clicks. It does not establish that a repeated request will not repeat the side effect.

If you understand the failure, you can decide where to prevent duplication and what evidence confirms the fix. If you only describe the latest visible symptom, you can end up with several overlapping workarounds and no clear owner of the rule.

That is how you get a building held together by repairs to the previous repairs. AI did not invent that style of development. It can make the next layer arrive before anybody has looked behind the first one.

If the only way you can change a system is to ask an assistant what it does, you have not finished understanding the thing you are shipping.

The tests can share the mistake

“But it generated tests too.” Good. Now read them.

Suppose the requirement says that orders of at least ten items receive a discount. The implementation uses quantity > 10. The tests use five items and twenty items. Both pass. The customer ordering exactly ten still pays too much.

The missing work was selecting nine, ten and eleven from the requirement. Asking the same assistant to generate more assertions around its own implementation may just produce a more thorough description of the same mistake.

I want tests that have a reason to disagree with the code. Expected behaviour should come from the rule we agreed to implement, not merely from whatever the implementation currently returns.

Ask what would prove it wrong

For the invoice feature, that means testing another customer's identifier. For the discount, it means the threshold. For a payment, it means the awkward outcome between “nothing happened” and “everything succeeded”.

An assistant can help identify those cases. It can save time writing the setup. But somebody still needs to decide whether the important risks are represented. The existence of a test file tells me almost nothing on its own.

A green build is useful evidence only when we understand which questions the build actually asked.

You still have to review it

Generating code faster than you can review it creates a queue. It does not eliminate the review.

A large generated change can be especially difficult because it may combine the requested feature with unrelated decisions: another dependency, a new abstraction, a changed error format and a small adjustment to the tests that makes everything pass again.

The developer sees a completed task. The reviewer inherits a pile of decisions to reconstruct. If nobody budgets for that work, “AI made us faster” may simply mean that the scrutiny disappeared from the schedule.

The boring files matter

I read the lockfile changes, configuration and migrations. A package introduced to solve a trivial formatting problem still becomes something we depend on. A database migration still has to work against existing records. An error handler that returns an empty array may make the UI look calm while concealing an outage.

I also read deleted assertions and weakened types. If the assistant fixed the build by making the check less demanding, we have changed the question instead of answering it.

Do not review the screenshot and approve the repository.

The second developer pays

The first version is only one moment in a product's life. Someone will need to change a rule, upgrade a dependency or explain a production failure. Often, that person did not write the original prompts.

They need a system with recognisable boundaries and decisions they can follow. They should not have to reverse-engineer why three nearly identical services handle the same concept differently.

The cost of that confusion belongs in the delivery story. It is easy to celebrate an afternoon saved on implementation while ignoring several days later spent discovering which generated assumptions the product now relies on.

Try one ordinary change

Here is a useful challenge for an impressive prototype: change a business rule without regenerating the feature.

Make the invoice visible to several employees of the same customer. Add a cancellation path. Change which actions are permitted after an order has been approved. Can you identify the relevant rule, modify it and explain why the rest still works?

If the answer is “we will ask AI to rewrite that part”, ask what will make the rewrite safer than the code it replaces. Generating a second implementation does not automatically explain the first one.

Where I would use it

Exploration is valuable. A disposable prototype can reveal that a proposed interaction is confusing before a team invests in building it properly. A small personal utility can be worth making even if it will never become a polished commercial product.

The acceptable shortcuts depend on what happens when it fails. A script organising copies of my own files has a different risk profile from a portal exposing customer documents. Give experiments boundaries: synthetic data where possible, limited access and a clear decision about what happens before anyone relies on them.

The problem is the quiet promotion from “look what we can demonstrate” to “customers are using it now”. “We will harden it later” becomes much less convincing when real data has already moved in.

Keep AI useful

I prefer asking for one bounded change, with an acceptance criterion I can verify. Let the assistant read the existing code. Challenge its proposed approach. Review the diff and exercise the failure cases before adding the next request.

Use it to explain an unfamiliar API, compare approaches, produce repetitive setup or suggest cases you may have missed. Keep checking the explanation against the actual behaviour. Being articulate is not the same as being right.

The point is to shorten the mechanical work while keeping the decisions visible. I do not need to type every line myself. I do need to know what I am approving.

Who takes the call?

When a customer reports missing data, the prompt history does not take the call. The person or team responsible for the product does.

That responsibility includes understanding the failure, limiting the damage and making a correction that will hold. You cannot outsource it to a tool that has no stake in the result. You certainly cannot discharge it by explaining that the demo worked.

So measure the whole change: implementation, review, deployment, corrections and the next developer's ability to work with it. Code appearing on a screen is only one part of that work.

The strongest case for AI in development is that it can give us more room to think carefully about the parts that matter. The weakest case is that we no longer need to think about them at all.

I want the first. The second is a freshly painted facade, and I am not going to call it a finished building.

Comments

There are no comments yet, leave yours below.

Leave a comment

Do you have an addition, question or experience related to this article? Share it below.

Comments are briefly reviewed before they appear.

Read more about:

The paradox of AI in web development

The paradox of AI in web development

AI makes writing code faster, but what happens to the experience developers gain by solving problems themselves? I look at how my workflow has changed and why juniors, mediors and seniors face different consequences. Greater output does not automatically mean stronger engineering judgement.

Continue reading

The bullshit of code coverage

A test suite can reach 100% coverage and still charge the wrong shipping fee or take a payment twice. Through boundary cases, misleading mocks and missing requirements, I examine what coverage actually proves. The useful question is not whether the code ran, but whether the tests would catch the mistakes your customers care about.

Continue reading

Security by design in Angular

Security by design in Angular

A route guard and a green pipeline do not stop one customer reading another customer's invoice. Using Angular and a .NET API, I turn a security requirement into server-side ownership checks and tests. Security by design starts with boundaries and evidence, before choosing a scanner.

Continue reading