Clean Code vs Vibe Coding (or how not to end up in the news)
The emergence of large language models (LLMs) in software development raises a fundamental question: are our established clean code practices still relevant against “vibe coding”? And more importantly, how do we avoid ending up on tech news with the next “oops, I exposed 50 million passwords”?
These old habits that save us
Clean code relies on proven principles that, incidentally, kept quite a few developers from technical unemployment:
DRY (Don’t Repeat Yourself) prevents us from duplicating our vulnerabilities across the codebase. SOLID helps us create architectures where one security flaw does not turn the whole system into digital Swiss cheese. KISS (Keep It Simple, Stupid) limits attack surfaces, because complex code often hides unpleasant surprises. YAGNI (You Aren’t Gonna Need It) prevents us from adding useless code that only increases attack surface without adding value.
These approaches favour centralized validation, coherent access controls and that little voice in our head saying “wait, are we actually validating this user input?”
When inspiration meets exploitation
“Vibe coding” has democratized development, allowing anyone to create functional applications in a few hours. That is great… until you realize “functional” does not mean “secure”.
LLMs excel at producing code that works, but they sometimes have a complicated relationship with security best practices. They can generate code vulnerable to SQL injection, XSS, or data leaks, especially when the security context is not explicitly mentioned in the prompt.
Current events catching up: the vibe coding disasters
Recent news is full of examples where the “it works, ship it” approach went badly. Startups discovered their AI-generated APIs exposed sensitive data, mobile apps were compromised through insufficient validation and some developers learned the hard way that “move fast and break things” should never apply to authentication systems.
The problem? LLMs optimize for functionality, not security. They can produce a login system that works “perfectly” while storing passwords in plain text or forgetting to check permissions. Worse, they tend to generate “just in case” code: extra endpoints, features “that might be useful”, creating potential entry points for attackers. A fine example of ignoring YAGNI costing real money.
The LLM and security paradox
Here is the core of the problem: clean code practices, with their centralized validations and security abstractions, do create more complexity for LLMs. But that complexity is often our first line of defence!
A well-abstracted authentication middleware may seem “opaque” to an LLM, but it centralizes security controls. Centralized validation functions may consume more tokens to analyse, but they guarantee all inputs are processed consistently under the same rules.
Conversely, more direct “vibe” code may be transparent to LLMs, but it multiplies potential failure points.
TDD: the guard-rail vibe coding was waiting for
Test-driven development is not a purist practice: it is the mechanism that makes vibe coding safe. The pattern:
- Write the tests FIRST, before asking the LLM to generate code. The tests are the specification, not after-the-fact validation.
- Include security tests in the specification: SQL injection, XSS, buffer overflow, permission denied, input edge cases. The LLM does not know your threat model unless you give it to it and the tests ARE the threat model.
- Generate the code with the LLM, giving it the tests as context. The LLM optimizes for passing the tests, not for “working”.
- Run the tests. A failure is a vulnerability caught before deployment, not after.
Red-Green-Refactor becomes Spec-Generate-Verify: the spec is your test, the generation is the LLM, the verification is the test passing. Without TDD, the LLM’s code goes to production untested. With TDD, the LLM’s code goes through exactly the same filter as human code.
And the cost is negligible: writing the tests BEFORE takes 10 minutes, fixing the SQL injection discovered in production takes 10 days.
The strict compiler, the other guard-rail
TDD sets the spec, but it only rules the moment you run the tests and in between there is a line of defense vibe coding almost always skips, the compiler, set to its strictest and most verbose.
A type checker catches the LLM’s mistake at compile time, where it costs nothing and rejects the stray None, the sketchy cast or the forgotten branch.
Turn on static typing everywhere the language allows it, mypy in --strict, TypeScript in strict mode, Rust’s borrow checker that never negotiates and crank the compiler warnings to the maximum, -Wall -Wextra -Werror in C, the linters in paranoid mode elsewhere.
The LLM happily produces code that works in the nominal case and falls apart on the edge case and an unforgiving compiler turns that edge case into a build error rather than a production incident.
It is a near-free guard-rail, alongside TDD rather than instead of it, because it works while you type, with nothing more to write than the types themselves.
Security as the referee
Computer security clearly favours clean code, but with important nuances:
Security patterns must be explicit. Instead of hiding security logic in complex abstractions, make it visible and documented so LLMs can understand and respect it.
Validation must be centralized but redundant. Centralize validation logic (the DRY principle) while adding extra checks at critical points - even if that seems contradictory, better one validation too many than a gaping hole.
LLMs need explicit security context. A prompt that says “generate me an API” without mentioning authentication, authorization, or validation will probably produce vulnerable code.
Towards responsible vibe coding?
The future lies in “responsible vibe coding” that combines LLM speed with clean code guard-rails:
Secure templates: Create code models with best practices built in that LLMs can reuse.
Security checklists: Systematize post-generation checks as you would for human code.
Automated security tests: Integrate vulnerability scanning tools into the AI-assisted development workflow.
Prompt training: Learn to systematically include security requirements in our interactions with LLMs.
Strict typing and an unforgiving compiler: alongside TDD, as described above, a near-free filter that catches at compile time what the LLM slips between the lines.
TDD first: Write the tests before generating the code, with security tests included in the spec. The LLM passes the tests or the code does not ship.
Conclusion
The compatibility between clean code and vibe coding exists, but it requires awareness: development speed must never come at the expense of security. LLMs are excellent assistants, but they need human guard-rails to avoid disasters.
Because in the end, nobody wants to be the developer explaining to their team how a SQL injection leaked the client database, especially when a chatbot wrote the offending code.
Friendly reminder: Your users trust you with their data. Do not disappoint them to save a few hours of development.