The Part of Vibe Coding Nobody Talks About

It’s not a vibe without the discipline


It’s been close to a year now since I started using Claude Code. I was always fascinated by the idea that LLMs would code for us. At the start I tried multiple coding agents before I got hooked on Claude. In spite of being the most mature coding agent, it has its peaks and valleys. There are times it took me down a rabbit hole I could not climb out of. It’s not that what it built was wrong — I reached a point where I could not understand the code anymore. That led to abandoned projects. And yet, at the same time, there were cases where I struck gold.

My journey was not smooth, but I learnt how to steer. I am sure many of you have fallen into these same pitfalls. This is not Claude’s fault. Software engineering is messy. Most projects start with a simple goal, and slowly we face decisions, trade-offs, and unanticipated blockers. As we push forward, things get complex.

Early on I learnt not to let it run on its own. I stayed away from auto-accept. I read every line of code Claude wrote, thinking through each one alongside it — more like pair programming than delegation. But LLMs generate code so fast that it takes a toll on you. I learnt to slow down and take breaks. It helped keep Claude focused. I could spot when it started to drift, usually because of an increasing pile of stale context from a long session. I also noticed the instructions I was repeating — between sessions, sometimes within the same session. I didn’t mind. I had accepted it as a limitation of the tool.


It was like driving a Ferrari in Bangalore traffic. My foot was constantly on the brakes.

I wanted to change that. I wanted to give Claude more autonomy, but I was afraid of losing control. With every new model release things were getting better, but I was still clinging to my old habits. That friction made me start researching how others were tackling this problem. That’s when I discovered slash commands and the idea of encoding my development workflow directly into a CLAUDE.md file.

I wanted to understand these first-hand — not just read about them. So I came up with a personal project, with the goal of finishing it over a single weekend. I wanted to build an application without reading a single line of code. I wanted to enter the realm of true vibe coding, without the fear of reaching the point of no return.


Here is a problem I had always wanted to solve. My Gmail account is 20+ years old. Over the years it had become a digital attic — newsletters I never read, promotional emails from brands I barely remember signing up with, and threads that go back to my college days. It was close to hitting storage limits. I had been meaning to clean it up for years, but the sheer volume made it feel like a task I would never start.

This was my project.

The goal was simple: build a personal web tool to visualize my Gmail storage, bulk delete the junk, and unsubscribe from mailing lists I no longer cared about. Nothing deleted without my explicit approval. A weekend project.

How complex could it get?

Claude went into planning mode and suggested google-api-python-client — a no-brainer, Python is great for a weekend project. Then came the UI choice: Streamlit, FastAPI + Next.js, Flask + Jinja templates. We deliberated on the options, weighed the trade-offs, and decided to start with Streamlit. Quick to prototype, Python-native, no context switching. We also agreed up front that if Streamlit ever became a blocker, we’d switch.

I carried forward a few practices from previous experience. After the plan was built, I asked Claude to review it as an Architect. This helped surface gaps I had missed — open questions about OAuth handling, edge cases in the sync logic, things I would have only discovered mid-build. I also asked Claude to break the plan into epics → stories → tasks, using a layered but linear approach so I could see incremental progress. Small, testable features. No cognitive overload — for me or for Claude.

Since Claude Code introduced throttling and exposed Opus as a dedicated thinking model, I had been switching between Opus and Sonnet to get the most mileage per session. Opus for planning, brainstorming, and architectural review. Sonnet for coding. The problem was that Claude would finish a plan and immediately start implementing, leaving no natural moment to switch models. I could always hit Escape — but that was still me pressing the brakes.

I wanted Claude to press the brakes for me.

So I added my first instruction to the CLAUDE.md file:

## Model Usage Preference

### Before Planning Mode
When I ask you to plan, create a plan, analyze an approach, or break down a task:
- Before doing anything, say exactly:
  "🧠 PLANNING MODE: This is a good time to switch to Opus for deeper reasoning.
   Type 'yes' to continue with current model, or switch to Opus first."
- Wait for my response before proceeding with the plan.

### After Planning is Complete
When you finish producing a plan and I confirm it:
- Say exactly:
  "✅ PLAN COMPLETE: Consider switching to Sonnet now for faster,
   cost-efficient implementation. Switch models and then tell me to proceed."
- Do not start implementation until I explicitly say "proceed" or "start".

This worked. Now Claude reminds me to switch models — I don’t have to remember.

Next I wanted to enforce TDD — Test Driven Development. Failing tests written before the first line of implementation. Every critical part of the system protected by test cases. If Claude accidentally broke a previously working feature while building a new one, it would get flagged immediately.

This was something I had skipped on past projects. As they grew, I craved it. And I had made the classic mistake — prioritizing new features over building a test suite, convincing myself I’d test everything manually. Now I live in a perpetual state of low-grade fear: testing beyond the scope of every change, haunted by the thought that something quietly broke somewhere I didn’t look.

Not this time.

## Development Workflow — TDD is Non-Negotiable

You MUST follow strict TDD for all feature development:

1. Before writing any implementation code, write a failing test first
2. Show me the failing test and wait for my confirmation before proceeding
3. Write the minimal implementation to make the test pass
4. Refactor only after tests are green
5. Never write implementation and tests simultaneously

If I ask you to implement something directly without mentioning tests,
remind me of TDD and ask: "Should I start with the failing test first?"

Red → Green → Refactor. Always.

Within a few hours I had a working dashboard. I was moving fast and confident. This was exactly what I had hoped for.

Then reality arrived.


Streamlit’s UI looked rough. Claude ran into problems implementing Gmail-based OAuth — Streamlit couldn’t handle redirects cleanly. There was a manual workaround and I lived with it. But then came the bigger problem: Streamlit has no real async support. Long-running operations blocked the entire UI thread. I had nearly 190,000 emails. A full sync at Gmail API’s rate limits would take close to 106 minutes. No progress bar. No way to let the user do anything while it ran. No way to stop it midway.

Streamlit had to go.

I pivoted to FastAPI on the backend and Next.js on the frontend. What started as a single-file prototype became a proper application — background threads, server-sent events (SSE) for live progress, a resumable sync that could pick up exactly where it left off after an interruption.

In one weekend, my simple Gmail cleaner had become something I needed to architect.

The point isn’t the engineering. The point is that this is what always happens — and managing it is the real skill.


This is where the CLAUDE.md and TDD practice paid off in ways I hadn’t expected.

The technology pivot — from a single Streamlit file to a two-tier FastAPI + Next.js architecture — happened without rework. The 311 unit test cases we had built gave Claude a safety net. It refactored confidently, and so did I. I enabled auto-accept and let it restructure the codebase without hovering over every line. The model switching between Opus for planning the migration and Sonnet for implementing it kept the sessions focused. Long sessions stopped being a problem.

I was no longer pressing the brakes. Claude was.

Around this time, I also invented three slash commands — partly out of necessity, partly out of habit.

My personal laptop is not really personal. My wife and kid take it over regularly. My Claude sessions get closed without warning. I needed a way to save my session state before handing over the laptop and resume cleanly when I got it back. That became /handoff and /cont…

$ cat handoff.md

Review our entire conversation and the current state of the codebase.
Write a structured handoff note to CLAUDE.md under a "## Last Session" section covering:
- What we were trying to accomplish
- What was completed
- What is in progress and its current state
- Known issues or blockers
- The exact next step to resume without losing context

This is critical — treat it as a baton pass to future-me.
$ cat handoff.md

Review our entire conversation and the current state of the codebase.
Write a structured handoff note to CLAUDE.md under a "## Last Session" section covering:
- What we were trying to accomplish
- What was completed
- What is in progress and its current state
- Known issues or blockers
- The exact next step to resume without losing context

This is critical — treat it as a baton pass to future-me.

Drop these in your .claude/commands/ folder and restart your Claude session. Type /handoff before you stop. Type /cont… when you’re back. The triple dot is a personal touch — I just added four dots to the filename. Small thing, but it’s mine.

The third command was /tdd. I realized all my projects would benefit from it, especially older ones where I hadn’t set up CLAUDE.md with TDD rules. This one takes an argument — the name of the feature or phase you’re building.

$ cat tdd.md

# TDD Feature Start

We are starting implementation of: $ARGUMENTS

Follow this sequence strictly:
1. Analyze what needs to be built
2. Write the test file first — failing tests only, no implementation
3. Show me the tests and STOP. Wait for my explicit approval.
4. Only after I say "proceed" — write the minimal implementation
5. Run tests, show results
6. Refactor if needed, keeping tests green

Do not skip steps. Do not write implementation before I approve the tests.

Software is messy. It always has been. The moment you think a problem is simple, it is preparing to surprise you. That is not AI’s fault. That is just the nature of what we build.

What AI coding tools give us is not simplicity — it is speed. And speed without discipline is how you end up in a rabbit hole you cannot climb out of. I know, because I have been there.

The slash commands did not make me a better vibe coder. The CLAUDE.md instructions did not make Claude smarter. What they did was take the discipline I already had as a software engineer — TDD, clean handoffs, focused iterations — and make it impossible to skip when I was moving fast.

That is the thing nobody tells you about vibe coding. The vibe is not the absence of engineering discipline. The vibe is what you feel when the discipline is so well encoded that you no longer have to think about it.

The Gmail cleaner is not done. My inbox is still full. I have no regrets.

Three slash commands, a CLAUDE.md that holds the rules, a model switching habit that costs nothing and saves hours. That is the deliverable I am taking with me. Every project I build from here gets these by default. The fear of losing control? Gone. The repetition between sessions? Gone. The anxiety of “what if this breaks something I built last week?” — covered by 311 tests that Claude wrote because I told it it had no choice.

Stop babysitting. Start engineering your workflow.

What discipline are you leaving behind when you pick up speed?