How I Use AI

By now you have probably heard people argue about how to code with AI as if there were a right and wrong way to do it. Vibe coding is the famous one, coined by Andrej Karpathy early 2025: you describe what you want, the AI writes it, you don't look too closely. It was supposed to be for weekend hacks but of course people started shipping it to production. On the other end there is the spec-driven crowd, who want you to plan everything first, sometimes with the AI helping you write the plan, then execute in small chunks. Then there is the "AI as pair programmer" stance, which really just means use it aggressively but keep your wits about you, and the agent-driven approach where you more or less tell Claude Code or Cursor to go handle it and come back when it is done. The truth is none of these are mutually exclusive and they are not even describing the same thing. Some are about engineering process, some about tool interaction, some about what happens when you stop pretending you are in charge.

As far as organization goes, I still follow old principles, but fine tuned for AI's strengths: generating code is fast and cheap. So when I need waterfall, I parallelize massively. When I need pair programming, I fire up 3 or 4 harnesses at the same time and work on many features at the same time. When I need agile, sprints are 2 hours long instead of 2 weeks. Also, the threshold to trigger a full rewrite is much, much lower.

The bucket

For coding itself, here is how I do it. I think of the AI's context as a bucket. Not a fancy term, not a metaphor that needs unpacking. A bucket. You throw things into it: your prompts, the files you want to edit, results from tool calls, whatever the AI spat back at you last round. The bucket is the entire working memory of the session and everything the AI does, it does based on what is in that bucket right now. That simple mental image, a container you fill and empty, is more useful to me than any methodology or framework I have seen people recommend. Because once you start thinking of it as a bucket, you start asking the right questions. What is in there right now? Does it still need to be? What is missing? Am I about to throw in something that will confuse the AI by sitting next to something else that contradicts it?

So the game becomes about curating the bucket. I do not let it accumulate. At each step I have a clear picture of what is inside and I keep it trimmed to exactly what the AI needs for the task at hand: the code I want it to work on, and whatever context is required to make a good decision on the next step. Nothing more. If something served its purpose in a previous exchange but is no longer relevant, it should not still be in there competing for attention. The bucket is not a journal. It is a workspace, and like any workspace it gets messy if you stop paying attention.

The problem is that the bucket is not static. Every time you send a message, the AI responds, and all of that goes back into the bucket. After a while you are carrying around a long history of exchanges, and at some point the harness will compact it -- summarize older parts, drop things, rewrite the conversation to fit within limits. That is where things drift. A detail you mentioned an hour ago gets smoothed over, a nuance gets lost, and suddenly the AI is operating on a slightly different version of reality than the one you intended. To fight that I use anchors. These are stable pieces of context that do not get summarized away: markdown files that the AI can read fresh on each turn, skill definitions, agent configurations, any mechanism that injects context conditionally rather than letting it decay through conversation history. An anchor says: this is an invariant, it is always true, go back to it when working on that thing. The bucket drifts, anchors do not.

Planning

And then there is the question of what I am trying to build in the first place. That, I keep in my head. I do not put the plan into the bucket. This is the part that most people find strange, and I understand why -- the common advice is to be explicit, to lay out your intent so the AI can align with it. But I have found that putting the plan into the context creates more problems than it solves. The AI is suggestible. If you tell it where you are going, it will start optimizing for that destination, sometimes at the expense of the step it is supposed to be taking right now. Worse, the plan changes. As the work progresses I discover things, adjust, take a different route. If the plan is in the bucket, it is now stale, and I have an AI that is trying to follow instructions that no longer match what I actually want. By keeping the intent outside the context, the AI solves the immediate step on its own terms, and I steer by choosing what to feed it next. The plan is a living thing. It lives in my head, not in the bucket.

Plans rot in the bucket faster than anything else. You write a step-by-step plan, maybe with code snippets showing roughly what each step should produce, and then by the time you reach step four the code from step two has been rewritten twice and the snippet in the plan now contradicts the actual codebase. The AI sees both, tries to reconcile them, and you end up debugging not a real problem but a conflict between your past self and your present one. Even without code snippets, a detailed plan in the context becomes an anchor for something that no longer exists. You wrote "refactor the auth module, then update the API endpoints, then add tests." But after refactoring auth you realize the API endpoints need a different approach entirely. The plan still says the old thing. The AI still reads it. The whole point of keeping the intent in my head is that it can change without leaving artifacts that the AI will trip over. The bucket should contain the present, not a record of decisions I have already moved past.

All of this only works if the exchange stays fast. I do not write long elaborate prompts and wait five minutes for a treatise in return. I keep exchanges small and tight, firing off short requests and reading the answers immediately. This is not impatience, it is a feedback loop. The quicker I can see what the AI did with what I gave it, the quicker I can tell whether the bucket is in good shape or something is off. A long slow exchange means I am sitting there wondering whether the AI understood me, which means I am not in control of the context. Small steps, quick turns, constant course correction. The session should feel like a conversation, not a letter exchange.

Checking

But how do you know the bucket is actually correct? You check. I throw verification questions at the AI, things where I already know the answer or where I can work out what the answer should be through deduction. Not trivial stuff, but questions that require the AI to actually think about what is in the bucket and connect dots. If it gets it right, good, the context is clean. If it gets it wrong, something in the bucket is broken -- maybe an earlier edit introduced a contradiction, maybe compaction dropped a critical detail, maybe I fed it something misleading three turns ago and forgot. The wrong answer is not a problem with the AI, it is a diagnostic tool. It tells me the bucket is lying to me and I need to fix it. One important caveat though: never ask the same verification question twice. The AI will have the previous exchange in its context, so the second time around it is not reasoning from the codebase, it is remembering its own answer. That tells you nothing about the state of the bucket.

And sometimes the bucket just gets dirty and you need to start fresh. Not the whole session, but specific lines of investigation. If I need to debug something, explore an alternative approach, or just experiment without committing to a direction, I fork the session. Forking means starting with a clean bucket: no history of dead ends, no failed attempts, no half-baked ideas. All of that stuff leaves residue. You tried something, it did not work, you moved on. But the AI still carries the entire conversation in its context and is now subtly biased by everything that came before. It will avoid approaches it already tried, it will be drawn toward things that seemed promising even if they are not, it will carry assumptions from one experiment into the next as if they were facts. Forking gives you clean water. The main session stays focused on the actual work and the forks are disposable. You learn what you need, and if the result is worth keeping you bring it back into a fresh context. Otherwise you throw it away, and nothing gets contaminated.

None of what I just described is how the tools want to be used. Modern AI coding harnesses are designed to achieve a result at all costs. The model is incentivized to produce output, to keep moving, to never stop and say "I am not sure about this." I try to override that behavior where I can -- rules, system prompts, configuration -- but it only works to some degree. Underneath it all, the model still wants to deliver, and it will cut corners to get there if you are not watching. So I watch. I monitor the chain of thought and the produced output closely, because if I don't, the harness will happily ship me something that looks like it works but is held together by assumptions and duct tape.

The most common way this shows up is silent laziness. The model leaves TODOs in the code without mentioning them. It implements half a feature and moves on as if that were the whole thing. It takes shortcuts that it never tells you about, workarounds that solve the immediate error but create a mess downstream. It makes design decisions on its own, architectural choices that a human colleague would at least flag or ask about, and just rolls them into the commit as if they were obviously what you wanted. You come back to the code an hour later and find that the data model has been restructured, a new abstraction layer appeared out of nowhere, and something you explicitly asked for has been silently replaced with an approximation. The harness calls this progress. I call it drift.

Hopeless problems

And then there are the things LLMs are just structurally bad at, no matter how much you curate the bucket. Object oriented programming is one of them. OOP is about generalizing code by altering existing structure: pushing behavior up the hierarchy, pulling common logic into base classes, rethinking what belongs where. LLMs do not want to do that. They want to generate new code, not restructure old code. So instead of refactoring the existing class hierarchy, they will add a helper method. Then another. Then a utility class with a name like `CommonUtils` that slowly becomes a dumping ground for everything the model did not want to think too hard about. It rarely questions how the existing code works. It reads it, accepts it as given, and builds around it rather than into it.

They are also bad at minimizing data flow. A well-designed system passes as little data as possible between its parts, because every piece of data crossing a boundary is a coupling point, a source of bugs, a thing that has to stay consistent. LLMs do not think in those terms. They will happily pass entire objects around, stuff everything into a shared state, create interfaces that exchange more information than needed just because it is easier to generate. The result is code where every component knows too much about every other component, and changing anything requires updating everything.

Related to that, LLMs are terrible at building features by removing things. Sometimes the best way to add a feature is to remove a limitation, simplify a data model, or cut out a layer of indirection that no longer serves a purpose. But the model's instinct is always to add. A new flag, a new parameter, a new conditional branch. Removing code feels like losing progress to an LLM, so it avoids it unless you explicitly force the issue. And you have to force it hard, because the model will fight you -- it will find ways to keep the old logic around "just in case" or quietly re-add the thing you told it to delete.

The worst offender though, the thing that more than anything else turns AI-generated code into an unmaintainable blob, is state proliferation. LLMs love ad-hoc state variables. A `processed` boolean here, an `itemCount` there, a `isInitialized` flag, a `hasBeenCalled` marker. Every time the model encounters a situation where something needs to be remembered or tracked, it reaches for a new variable rather than asking whether the existing state model can express it. This is how you end up with objects that have twenty boolean fields and nobody can remember what combination of them represents a valid state. It is how you get functions with side effects that depend on three different counters being in sync, and when they fall out of sync there is no obvious way to debug it because the state was never designed, it just accreted. This is the big ball of useless code, and its primary ingredient is state that nobody asked for, nobody designed, and nobody can reason about after the fact.

Learning

None of this is something you learn by reading about it. Working with AI is closer to learning an instrument than following a tutorial. You practice on small things, you fail, you develop a feel for what works and what does not. There is no substitute for hours spent watching the model do things and gradually learning to anticipate where it will go wrong. The big models, the impressive ones that can hold entire codebases in context and generate working features in one shot, are actually not great for learning. They are too good at covering their mistakes. They produce polished output that hides the reasoning failures underneath, and by the time you notice something is off the damage is already spread across three files.

What taught me the most was going in the opposite direction. I started using very small models, twenty to thirty billion parameters, running on a local GPU. These models are not impressive. They struggle with anything non-trivial, they lose the thread constantly, and they will confidently hand you broken code with a straight face. But that is exactly the point. Because they are weak, you cannot rely on them, which means you have to stay close. Every step needs checking. You learn to go slow, to break things into tiny pieces, to notice the exact moment the context starts going sideways. You learn to recognize the particular flavor of evasion when a model does not understand something but does not want to admit it -- the slightly vague answer, the helper method that technically addresses the prompt but avoids the hard part. The big models do the same thing, they are just better at hiding it. The small models taught me to see it, and once you can see it you cannot unsee it, no matter how capable the model in front of you happens to be.