Unlearning How I Write Code
- #engineering
- #ai-agents
- #claude-code
- #tooling
Some time this year I noticed the hardest part of a task had moved. It used to be writing the code. Now it's the stretch before any code exists, when an agent asks me question after question about what I want and I answer until there's nothing left to guess. I find that stretch tiring in a way typing never was. Then the code comes back, I read it against what I said I wanted, and most of the time it merges.
Andrej Karpathy coined "vibe coding" in early 2025 for the habit of accepting whatever the model writes without reading it. A year later he wrote that the professional version deserved a different word, and offered agentic engineering:
"agentic" because the new default is that you are not writing the code directly 99% of the time, you are orchestrating agents who do and acting as oversight. "engineering" to emphasize that there is an art & science and expertise to it.
I'm not sure the name will stick. The distinction will. Vibe coding is when you don't care about the code. This is when I care exactly as much as I always did and have stopped being the one typing it. Getting there meant dropping seven habits, most of which I was quite attached to.
1. Starting with code
The most expensive thing I ever handed an agent was a sentence like "add auth to this endpoint". I knew what I meant. The agent didn't, so it decided for me, one small decision after another, and I spent the afternoon in the review undoing the ones I disagreed with, one comment at a time. I did this more than once before it sank in that the afternoon was my fault.
So now the first thing I do is get interrogated. An agent asks me one question at a time, never a batch, until nothing about the idea is still an assumption I haven't said out loud. Which roles can hit this endpoint. What happens on token expiry. How a test proves an unauthorized call was refused. It does this because a skill tells it to. I use Claude Code, where a skill is a markdown file of instructions the agent loads when I type its name, so the same procedure runs the same way every time, including on the days I'd rather skip it. Only after the questions does anything get written down, and what gets written is a record of decisions, not a wish list. Matt Pocock, whose skills mine are forked from, is blunter about it than I'd be on the page for to-spec, his skill that turns the interrogation into a written spec:
Anything the spec asserts that you never actually said is a defect.
My job was always deciding. I'd just been doing it in the wrong order, with the code as the medium, which is the slowest and most expensive medium there is for finding out you were wrong.
2. Keeping the conventions in my head
A new hire onboards once. An agent onboards every session, from zero, and it does not remember yesterday. Every "we don't do it that way here" that lived in my head got violated at the start of each run, and every time I was surprised, which in hindsight is the surprising part.
So the conventions go in a CLAUDE.md, a text file at the root of the repo that the agent reads at the start of every session. I'll call it the steering file. Boris Cherny, who built Claude Code, describes his setup as "surprisingly vanilla", and the part that does the work is one file:
Our team shares a single CLAUDE.md for the Claude Code repo. We check it into git, and the whole team contributes multiple times a week. Anytime we see Claude do something incorrectly we add it to the CLAUDE.md, so Claude knows not to do it next time.
Mine is longer than I'd like and I edit it most weeks. The same pressure lands on the code itself. An agent wants clear module boundaries, strong types, fast builds, and errors that say what actually went wrong. I've known this was good for humans for as long as I've done this job. It took a machine to make me do it.
3. Being the test runner
When the agent couldn't run the tests, it handed me code that didn't work, and I ran the tests. Prompt, wait, paste the error back, wait again. It felt like pairing. It wasn't. It was me being a slow, expensive shell script.
So the agent gets everything I use to check my own work: the linter, the unit tests, a headless browser it can drive from the command line. All of it has to be fast, because a slow loop for me is a slow loop for the agent, multiplied by every attempt it makes, and it makes a lot. An agent that can see its own failure loops on it until the tests pass. One that can't will tell you, with total confidence, that it's done.
4. Watching it work
One agent, one task, me staring at the terminal, is the same throughput as me typing, minus the typing. I did this for a while and called it a productivity gain. The gain showed up when the tasks got longer and the agents more numerous. "Implement this, write the tests, run them, don't come back until they pass" is a thirty-minute task. I can have up to seven of those running, a cap I set in my own tooling and haven't felt the need to raise. I stayed at one for longer than I should have, partly because it felt like losing control and partly because I was afraid of what seven of them would do with my credentials. The second fear was the right one and gets its own section. The first went away when I read Gergely Orosz's observation that overseeing parallel agents comes naturally to senior engineers, because they've spent years being the reviewer for a team's parallel work. That stung a little. I'd been that reviewer for years and hadn't made the connection.
An epic, a feature big enough to need a dozen tickets, gets cut into those tickets by another skill, and the tickets have dependency edges between them. The set of tickets whose blockers have all closed is the frontier, and every ticket on the frontier gets its own agent in its own git worktree, a separate checkout so they can't step on each other's files. When one merges, the frontier moves and the next batch goes out. Per-ticket merges land on an integration branch for the epic, and the one pull request from that branch into main is the one I review by hand. The skill is called /implement-with-agent-team; the mechanics are a post of their own. Reading the frontier and dispatching the next batch was never the hard part. I'd been doing it by hand for a month before that occurred to me.
Some numbers, because I'd distrust this post without them. Last ten weeks, the two repos I spend most of my time in. Five hundred and forty pull requests merged, about four hundred and ninety of them opened under my name by an agent I dispatched. Most were single tickets; eight epics account for about a hundred, each ending in one hand-reviewed pull request into main. Two of the five hundred and forty needed a follow-up fix within a day of merging, and nothing was rolled back. Two is low, and I'd have guessed higher. The integration branch catches most of the damage before main, and the eight epic pull requests are where I found the rest. Roughly four in five tickets built on the cheaper model; the rest went to the expensive one on purpose, for tickets where the spec left a judgment call the cheaper one kept getting wrong. The busiest week landed seventy-two merges in one repo. I read seventy-two pull requests that week, against seventy-two specs I'd already argued with an agent about. I was tired. It was a good week.
5. Approving tool calls one at a time
Clicking "approve" on every tool call is supervision theatre. I wasn't reading those calls. I was adding latency and telling myself it was oversight, and I kept it up for longer than I'll admit here.
Now I decide once which files an agent may write, which commands it may run, what it may reach on the network, and which credentials it holds, and Claude Code's permission settings enforce that instead of me. Production credentials are never on the list unless I've said so out loud that day. I started narrow and widened as the limits proved themselves, and the only thing that still needs a human is the action that can't be undone.
The most useful fence I have is a small hook, a script that runs right before a tool call and can block it. Mine runs whenever /implement-with-agent-team starts a new agent, and refuses the start if the model that agent will run on hasn't been named. It exists because the model defaults to whatever the spawning agent is running, so an expensive model would otherwise end up on a ticket sized for a cheap one, with nothing in the run's output ever saying so. Nothing told me. I noticed by accident.
6. Fixing the mistake instead of the cause
When an agent took a wrong turn, I used to correct it and move on. The turn came back the next session, because nothing about the session had changed, and I'd correct it again, slightly more irritated. Mitchell Hashimoto, who created Terraform, calls the fix "harness engineering", the harness being everything the agent runs inside: "anytime you find an agent makes a mistake, you take the time to engineer a solution such that the agent never makes that mistake again."
The run that taught me this was an epic of eleven tickets in July. Two of them each added a column to the same table, and the ticket graph had no edge between them, because I didn't notice they touched the same migration when the epic was cut. Both built cleanly in their own worktrees. Both merged onto the integration branch within a minute of each other, and the second one's migration fell over on the first one's schema. Nothing reached main. But I spent an hour finding out why, and the fix was not "be more careful next time", because I'd have been exactly as careless the next time. The fix was a check in the ticket-cutting skill that lists every file each ticket expects to touch, flags any two tickets that name the same migration or model, and refuses to finish until I've added a blocking edge or said out loud that the overlap is fine. It has fired four times since. Three of those I would have missed again.
The harness rots, too. When a new model ships, half the workarounds in it are for a weakness the model no longer has, and I'm bad at pruning them. This is the habit I put off longest, and it's the one that makes the other five cheaper the longer I keep it up.
7. Thinking this was about code
The same loop runs the design doc, the sprint summary, this post. Decide, hand off, verify, fix the harness. I resisted that for a while too, on the grounds that writing was different. It isn't, particularly.
The one I kept
None of this is permission to stop caring. If anything the bar goes up, because the volume goes up. Simon Willison's rule, from the piece where he drew the line around vibe coding, is the one I hold to: don't commit code you couldn't explain to someone else.
I still review every pull request. What changed is what I read for. Early on I read every line, to learn where the model was reliable and where it wasn't, and I'd tell anyone starting now to do the same for a while. That stopped being possible with several running at once. Now the agent that wrote the code reviews it itself before opening the pull request, checking what a checklist can check. My eyes go to what a checklist can't: whether the design will hold up at production traffic, what the change does to the systems upstream and downstream, whether the security boundary still holds. If it regresses in production, it's mine, whoever typed it.
My first weeks were slower and I nearly gave up twice. I spent them writing steering files and cutting modules until they explained themselves. Writing the code got cheap. Deciding what to write still takes the whole afternoon, and I don't think that's going to change.