Home → Engineering
Vibe Coding Rituals
"I finished it in three hours." Two days later we are debugging the same feature. The problem is not the tool — the tool is genuinely good. The problem is the absence of ritual.
- Vibe coding is great for prototypes and dangerous in product code. Make the distinction up front.
- The most common failure is "works but does not scale". Perfect with ten records, disastrous with ten thousand.
- Frequent commits are your undo button. Commit at every working step.
- In some places a human writes the final version: authorisation, money, deletion, data migration. Suggestions are fine; copy-paste is not.
An honest distinction first
I am not saying do not use AI; I use it and so does the team. But there are two different kinds of work and they cannot be done the same way:
- Prototypes, validating an idea
- One-off scripts, data transformation
- A first step in an unfamiliar library
- Test data, sample inputs
- A demo that will be deleted tomorrow
- Product code that will live for years
- Anywhere money is involved
- Authentication and authorisation
- Operations that delete or migrate data
- Flows handling personal data
Teams that cannot tell the difference ship prototype-speed code to production and pay the bill three months later.
Three failures from the field
Three different projects, hence three different languages. What they share: all of them passed testing.
1. A query inside a loop
A report screen was generated and opened instantly in testing. In production it took 40 seconds. The code was this:
orders = OrderRepo.find(start, end); // 8,400 records
for (o : orders) {
o.customer = CustomerRepo.findById(o.customerId); // ← 8,400 queries
o.lines = LineRepo.findByOrder(o.id); // ← 8,400 more
}
The test dataset had 12 orders, so 25 queries — nothing to notice. Production ran 16,801. This class of bug is called N+1 and it is almost always born the same way: a solution that is correct for one record applied to a list.
Ritual: for every piece of generated data-access code, one question — "what happens if this loop runs ten thousand times?"
2. A library that does not exist
For a helper function, the suggested package had a perfectly plausible name and
did not actually exist. npm install failed, it took two
minutes to notice. We got off cheaply; do not mistake that for the general rule.
The dangerous version is this: attackers take the package names AI tends to invent and
actually register them on npm and PyPI with malicious code inside. The name for
it is slopsquatting. Then npm install does not fail, it
succeeds; and because a postinstall script runs at install time, you are
hit before you ever call the code.
There is a quieter version too: suggesting a package that does exist but is abandoned, last updated four years ago. Install works, the code works, a security hole quietly walks in.
Ritual: when a new dependency is added, check four things — is the package really from the publisher you expect, or is its name an imitation of a known one; last release date; weekly downloads; open security advisories. It takes a minute.
3. Tests that do not verify the code
The subtlest one. After the code was written we asked for tests. Tests were written, they all passed. Then someone noticed: the tests were verifying what the code did, not what it was supposed to do.
// The code has a bug: the discount is subtracted as an amount, not a percentage
// code: price - discount
// correct: price * (1 - discount/100)
// A test written by reading the code: the code's output copied in as "expected"
test("applies discount", () => {
expect(applyDiscount(200, 20)).toBe(180); // the code gives 180, the test passes
});
// The correct answer is 160. The test has recorded the bug as "correct".
Ritual: write the test first, or at least decide the expected values without looking at the code. A test written by reading the code verifies itself, not the code. And do not leave the input to chance: on 100, 20% off gives 80 and subtracting 20 also gives 80. Pick an input that separates the two formulas.
Ten rituals
A confession first: in Andrej Karpathy’s definition, the one that coined the term, vibe coding means accepting generated code without reading it. Every one of the ten items below is about reading, questioning and testing. So technically these are not vibe coding rituals; they are rituals for the transition from vibe coding to disciplined AI-assisted development. The title stuck in the team, so it stays.
1. Fix the target first
Before asking for code, write two or three sentences describing "done": what goes in, what comes out, which edge cases matter. Those sentences sharpen the prompt and make "is it finished?" answerable at the end. Best of all is writing them as a test.
2. The small-step rule
Do not accept 400 lines at once. Ask for one function, one file, one behaviour. The reason is not quality but verifiability: you can read and understand 40 lines, you will not read 400 — and code you did not read is code that was not reviewed.
3. Give the project rules in writing
Keep a rules file in the codebase and pass it as context every session: which libraries are used, how errors are handled, what the log format is, how layers are separated.
# In this project
- Dates: java.time only. No Joda.
- Errors: our own exception class carrying an ErrorCode, never throw raw exceptions.
- Data access: no SQL outside the Repository layer.
- Logs: structured logging, no string concatenation into messages.
- Tests: at least one unhappy-path test per service method.
Writing this takes half an hour and it is the best antidote to the consistency drift. It also helps every new joiner, so it pays twice.
4. Do not accept it without asking "why?"
Ask for a rationale for every unusual line in the generated code. If the explanation is not reasonable, neither is the code. This ritual does two things: it catches bugs early and it makes you learn — which is the real long-term win.
5. Commit at every working step
The most practical item on this list. With AI, code changes fast, and past a certain point going back to "it worked two steps ago" becomes impossible.
small step → tests green → commit
small step → tests green → commit
# When it breaks:
git diff # uncommitted changes: this is the step that broke it
git reset --hard HEAD # restore tracked files to the last green commit
git clean -n # list the new files that would be deleted
git clean -fd # delete them once you are sure
# If you would rather set it aside, new files included:
git stash -u
Because you only commit while tests are green, the last commit is always the solid
one; HEAD, not HEAD~1. Two cautions: --hard
throws away uncommitted work for good; and it only restores files git tracks. New
files the AI created in the broken step stay exactly where they are and pollute the
next attempt; that is why git clean is part of the rhythm. Plain
git stash does not take new files either, you need -u.
Before the PR, tidy up with git rebase -i: fold the “typo” and
“try again” noise together, but do not squash everything into one giant
commit; keep meaningful steps so rollback stays possible. The criterion is not whether
the branch is pushed but whether it is shared: rebasing your own
feature branch and pushing with --force-with-lease is normal; a branch
someone else has committed to does not get rebased. It does not matter that the
intermediate commits are ugly; they are your undo button.
6. The green bar rule
Do not move to the next feature while tests are red. It sounds obvious, but the natural flow of vibe coding encourages the opposite: to keep the feeling of speed you say "I will look at that later". Three "laters" in and you have lost track of what broke when.
7. The dangerous zone list
The team rule is one sentence: in these places a human writes the final version. Our list:
- Authentication, authorisation, session handling
- Money: pricing, discounts, tax, refunds
- Operations that delete or migrate data (including migrations)
- Flows handling personal data
- Reconciliation with external systems
Taking suggestions is allowed; but you write the final line yourself, understanding it line by line. Copy-paste is not allowed. The criterion for making the list is simple: if it is wrong, can you undo it?
8. The three-attempt rule
If you are fixing the same error for the third time, stop. You are in a loop and the code is getting messier with each attempt. What to do: go back to the last working commit, read the error yourself, describe the problem, then start again.
This rule saved me the most time. Forty minutes in a loop costs more than ten minutes reading by hand.
9. Watch what you paste
Pasting logs while debugging is completely natural — and logs can contain customer emails, tokens, connection strings. Write the team rule down; without one, everyone invents their own in good faith.
10. Write it once yourself
If you are learning something new, read the generated code, close it and write the same thing yourself. You lose half an hour and the topic becomes yours. What I see in people who skip this: a year later they have shipped a lot and gone nowhere deep, and they freeze in the first real crisis.
What to look for in code review
Generated code has a smell. In review I look especially for:
| Signal | What it means |
|---|---|
| A query or HTTP call inside a loop | Fine on test data, fatal in production |
| A broad catch-everything block | Errors are being swallowed silently |
| A second helper class doing the same job | The existing codebase was not consulted |
| Comments restating what the code says | Usually harmless, but a sign nothing was reviewed |
| A pattern or library not used in the project | Consistency drift beginning |
| Tests covering only the happy path | The risky paths are untested |
So does it actually make you faster?
Yes, but it depends where you measure. Ours looked roughly like this:
- To a first working version: clearly faster. No argument there.
- To a reviewed version: the gain shrinks, because review load rises.
- To a version running cleanly in production: without rituals the gain can go negative.
So the speed is real; but what you should measure is not "how fast did I write it" but "how long until it was safely in production".
This is not just our observation. In a METR study run with early-2025 tools, 16 experienced open-source developers did 246 real tasks in their own repositories; each task was randomly assigned to be done with or without AI. At the end they believed they had been about 20% faster; the measured time was 19% longer. METR now says that result is out of date: with today’s tools there is probably a speed-up, but they cannot measure its size reliably. The lasting lesson is not the number: people’s estimate of their own speed can diverge seriously from the measurement.
Checklist
- Can I explain every line of this code?
- Is there a query or external call inside a loop?
- Do the tests verify the expected behaviour, or the current code?
- Is the new dependency really the package I expect, and is it maintained?
- Does it follow the existing patterns, or open a new path?
- Did I touch a dangerous zone? If so, did I write the final version myself?
- Are the commits split into meaningful steps? (One giant commit makes rollback hard.)
- Was there any secret data in what I pasted?
Conclusion
Vibe coding is not a skill but a mode. You need to know when to switch it on and when to switch it off. On in a prototype, off in a payment flow.
And back to "I finished it in three hours": that sentence was not wrong. What was missing was the rest of it — it was written in three hours, and nobody read it. All of these rituals are really trying to bring back one thing: reading.
For the gap between perceived and measured speed, see METR’s July 2025 study and the February 2026 update that marks its result as no longer current. Slopsquatting refers to supply-chain attacks that register the package names AI tends to hallucinate on real package registries.