DEV Community

Cover image for How I Actually Code with Claude Code: My Real Workflow on a Real Project
Gabriel Abreu
Gabriel Abreu

Posted on Originally published at codewithgabo.com

How I Actually Code with Claude Code: My Real Workflow on a Real Project

Real repo tasks and honest failure modes over hype

There are two kinds of articles about coding with AI. The ones that generate a sorting function and conclude the profession is over, and the ones that show a dumb bug and conclude none of this works.

Neither one looks anything like my actual workday.

This is the third kind: the concrete workflow I use with Claude Code on this very site, three tasks I actually delegated, the trail in the repository to back it up, and a section on where it fails that runs as long as the section on where it works. That's the part I would have wanted to read.

What it is, without the marketing

Claude Code is an agent that runs in your terminal, inside your repository. It reads your files, runs your commands, edits your code, makes your commits. It isn't editor autocomplete, and it isn't a separate chat window where you paste fragments back and forth.

That difference matters more than it sounds. An assistant that sees one file helps you write a function. An agent that sees the whole repo, runs the tests and reads the output can take a complete task off your hands. It's the difference between asking for advice and delegating work.

My setup

Nothing exotic:

  • The repo for this site: a React frontend on Vite, Sanity as the CMS, and a small backend of Vercel Functions.
  • A .claude/ folder in the project with the local config and permissions.
  • A docs/plans/ folder where every design and plan lives. This is the key piece, and I'll explain why in a second.
  • Git worktrees when a big task deserves isolation from the current working tree.

And one rule of my own, which is what actually changed my results: nothing big gets written without a written plan first.

The workflow: design, plan, execute

The temptation with an agent is to open the terminal and say "add me an admin panel." Sometimes that works. Often it produces something that runs but isn't what you wanted, and you find out after six hundred lines are already on disk.

So I split it into three phases, and I don't let them overlap.

One: design. Before touching code, a conversation. What problem are we solving, two or three approaches with their downsides, which one we pick and why. Out comes a design document in docs/plans/. Short, but written.

Two: plan. The design turns into a task-by-task plan. Which files each task touches, which test gets written first, which command verifies it, where the commit goes. My plans folder has files like 2026-04-28-slice-2-vercel-migration.md and 2026-04-29-portfolio-audit-implementation.md. Each one is a plan that got executed task by task.

Three: execute. Now the code. With the plan in front of it, the session doesn't drift. If something doesn't fit, it shows up right away, because there's a document saying what should be happening.

It sounds like bureaucracy, and I thought so at first. It isn't. The written plan is what turns "the agent did something weird" into "the agent went off script at step 4," which is a very different problem and a much easier one to fix.

Three tasks I actually delegated

The portfolio audit

I asked it to audit this site as if it belonged to a client. Read every public component, walk the routes on mobile and desktop, measure the build chunk sizes, count the real posts in Sanity.

It came back with 20 findings across four areas, each with a priority and an effort estimate. Among them: a 993 KB unoptimized illustration, a sitemap containing zero of nine published posts, and a 404 page that hung forever on "Loading post...".

I wrote that one up in detail in a separate post, but what matters here is the division of labor: the machine found, I prioritized. The document came back with findings and no fixes, on purpose, so I would be the one deciding what got touched and what didn't. Three of the twenty I threw out.

Migrating Express to Vercel Functions

I had a small Express server on Railway wrapping the Google Analytics API. It worked, but it cost money every month to sit there 24/7 serving a dashboard almost nobody visits.

Moving it to serverless functions was textbook work: pull the helpers into api/_utils/, turn the route into a handler, move the environment variables, verify the external contract didn't change. The frontend never noticed. The bill went to zero.

This is exactly the kind of task where delegating wins: mechanical, well defined, with an objective success criterion. It doesn't take judgment, it takes not getting twenty details in a row wrong. A machine does that better than I do at eleven at night.

The sitemap generated from Sanity

My sitemap was a static file I had to edit by hand every time I published. Which is to say: never.

Now it's a script that runs on prebuild, queries Sanity with GROQ, and writes the file with every post and its real date. Eighty lines of Node, no new dependencies.

Small task, and that's exactly why it sat there for months. That's the pattern I've noticed most: what has saved me the most time isn't the big tasks, it's the forty-minute ones that had been on the list for half a year because they were never urgent enough.

Where it fails

This is where most of these articles go blurry. Straight to it.

It accepts your premise too fast. If you say "fix this CSS bug," it will go looking for a CSS bug. If the real problem was route ordering, you may get a CSS fix that covers the symptom. Now, when something breaks, I describe the behavior and not my diagnosis. The difference in outcome is enormous.

It's too agreeable about your ideas. If you propose a bad approach, the default response tends to be helping you build it well. I explicitly ask for two or three options with their downsides before anything gets decided, because if I don't ask for alternatives, they don't show up.

Context degrades in long sessions. Over a session of several hours, the decisions from the first half hour get fuzzy. That's why the plan goes into a file instead of staying in the conversation: the file doesn't forget.

It doesn't know what ugly looks like. It can write a component that is correct, accessible, passes the tests, and looks bad. Visual judgment is still yours. On this site I've redone by hand a fair amount of CSS that was technically fine.

And the big one: you are still responsible for what gets merged. I read every diff. Not because I trust it less than a human colleague, but exactly as much as I'd check a human colleague. A commit with my name on it is mine, no matter who typed it.

When it costs more than it saves: tasks under ten minutes that I already know how to do, one-line changes, and anything where explaining the context takes longer than doing the thing. Writing a good prompt for a trivial change is net negative work.

How to start tomorrow

If you want to try this without getting burned:

  1. Start with a boring, well-defined task. Migrating a format, writing tests for something that already exists, updating dependencies. Don't start with your product's flagship feature.
  2. Ask for a plan before code. Even when the task is small. Reading the plan tells you in thirty seconds whether you were understood, and fixing a plan is free compared to fixing an implementation.
  3. Work on a branch or a worktree. So you can throw it all away without thinking twice if it goes sideways.
  4. Read the diffs. All of them. If you're not going to read them, don't delegate.
  5. Leave the context in the repository, in writing. Decisions that live only in a conversation get lost. The ones in docs/plans/ are still there three months later, when you no longer remember why you chose that.

What actually changed

I don't code faster. I code with less friction on the boring parts, which turn out to be most of the work.

The tasks that used to sit on the list out of inertia now get done, because the cost of starting them dropped enough. The sitemap had been pending for months. The audit for longer. Neither was hard; both were tedious, and tedious is exactly what piled up on me.

What didn't change: I still decide what gets built, I still review every line that goes in, and I'm still the one responsible when something breaks in production.

That seems right to me. It's the part of the job I like.

I write up the things I break and fix at codewithgabo.com.

Top comments (17)

Collapse
 
heinrichneb profile image
Heinrich Neb

The design → plan → execute split is the part I'd defend hardest, and Debashish's suggestion is the piece that makes it compound. I want to add one number to it and one failure mode you don't list.

On "the file doesn't forget." It doesn't forget what you intended. It has no mechanism for what turned out wrong.

I keep a store of technical lessons rather than plan files, and I went and counted it this week: 524 records, 257 of them (49%) have been overwritten at least once, 408 overwrites in total. Half of everything I wrote down needed correcting.

A docs/plans/ folder has no overwrite. A plan that turned out to be the wrong approach sits in the same directory as the one that worked, with the same weight, and three months later both read as history. That's Debashish's point exactly - the outcome note isn't a nice-to-have, it's the only thing that separates a record from a rumour.

One detail that made it work for me, in case it saves you a round: require the outcome note on updates, leave it optional on first write. I resisted required fields for years because humans fill them with "fix". But here the writer is usually the agent, mid-session, right after it diagnosed the thing - it has the reason in context, and the field costs it half a sentence. A required field filled by whoever just hit the problem tends to contain the diagnosis.

The failure mode I'd add to your list, because it's the one that survives your "read every diff" rule:

A check can be green and blind. I had a test asserting that a benchmark number appeared in my server's output. The number was a hardcoded string. The check was guarding the sentence, not the measurement - and a number the server doesn't compute can never fall. Green for weeks, while the real figure was ten points behind the naive baseline I was claiming to beat.

Reading the diff would not have caught it. The diff was correct. The test passed. Everything was consistent; it was just consistent about the wrong thing.

A sharper version happened yesterday: I renamed a module's transport - same rules, different words — and 7 of 13 tests went red while nothing had gotten worse. They were asserting identifiers, not behaviour. Same root cause: the check was watching the spelling.

This one is worth adding precisely because it's specific to delegation. An agent writes tests that pass. It is very good at making the thing you asked for go green. Whether the green means anything is a judgment call - the same kind as your "it doesn't know what ugly looks like", just less visible, because there's nothing to look at.

And your last observation is the one I'd build on. "What has saved me the most time isn't the big tasks, it's the forty-minute ones." That matches what I see, and it has an uncomfortable corollary: the small fixes are also the ones that leave nothing behind. A design decision gets a plan file. A forty-minute fix gets a commit and a shrug - and six months later nobody knows why that flag is set the way it is.

Your docs/plans/ closes that gap for the big decisions. I don't know of anything that closes it for the small ones, and by your own count that's most of the work.

Collapse
 
gabbs279 profile image
Gabriel Abreu

"A check can be green and blind" is going in the post. I have one from
this repo, three days old.

I set per post and hreflang on the two translated pairs. The
commit message says exactly that. I checked it in the browser:
documentElement.lang === "es" on the Spanish posts, three correct
alternate links, all present. Green.

The prerenderer that writes the served HTML rewrites only , and I
never taught it about either one. Grep for "hreflang" in that script at
the time: zero hits. So every crawler that doesn't run JavaScript got
on the Spanish posts and no annotation at all. The check
was watching the DOM. The thing that mattered was the response body. Both
are real; they just aren't the same surface, and I had verified the one
that was easier to reach.

Yours is the sharper version — your test asserted a hardcoded string,
mine at least measured something real, just not the thing that shipped.
Same shape though: everything consistent, consistent about the wrong
thing.

On requiring the note on updates and leaving it optional on first write:
that's the part I would have gotten backwards. My instinct was to require
it always, and you're right that on a first write there is nothing to say
yet — the outcome hasn't happened. That's how you get "fix". Requiring it
on the overwrite catches the writer at the one moment they're holding the
diagnosis.

Your last point I don't have an answer to. The forty-minute fixes are
most of the work and they leave a commit and a shrug. What I do is put
the reason in the comment next to the thing, on the theory that it's the
only artifact that travels with the code — but you just watched me
demonstrate that comments rot exactly like plan files. So that isn't a
solution. It's the same problem somewhere I happen to look more often.

Collapse
 
heinrichneb profile image
Heinrich Neb

Your hreflang case is the cleaner specimen of the class, because both of your surfaces were real. Mine was consistent with a hardcoded string; yours was consistent with the DOM - and the DOM is a genuine artifact, just not the one crawlers read. The rule I'm taking from your version: verify at the surface your actual consumer reads, not the one that's easiest to reach. The browser was one console.log away; the response body needed curl. Three keystrokes of difference, and the check silently measured the wrong world.

I hit the same shape twice more this week, if it helps your post: a deploy gate died with exit 127 - the check against silent tool failures itself failed on a missing tool. And a harvester of mine threw away successful work because it judged by exit code while the result file sat right next to it, seven records long. Both taught the same thing: judge by the artifact, not by the messenger. Exit codes, the DOM, commit messages - all messengers.

On your forty-minute fixes: I don't think the answer is a better place to write the reason - you just demonstrated that comments rot, and my plan files and memory server rot the same way when recall isn't automatic. What has survived for us is making the diagnosis executable: the fix ships with a check that would have caught the bug, and the why lives in that check's failure message - where the next person reads it at the exact moment they need it. A comment is a note next to the thing. A failing test is a note that interrupts you. Only the second one travels.

Which leaves the honest gap, and I'm curious where you land on it: some forty-minute fixes are genuinely un-checkable. For those we do a one-command capture at fix time, recalled automatically next session - and even for me, the finding is that capture only happens when it's cheaper than not capturing. Discipline doesn't survive; defaults do. So two questions back: roughly what fraction of your forty-minute fixes do you think could have shipped with a check? And would requiring "check or one-line lesson" at PR time feel like a guardrail to you - or like process tax that you'd start routing around within a month?

Thread Thread
 
gabbs279 profile image
Gabriel Abreu

"Judge by the artifact, not by the messenger" is the line I'll be
repeating. I want to add one messenger you didn't list, because it's the
one I believed: the commit message. Mine said "emits hreflang links." It
was true. It was also the reason I stopped looking.

On the three keystrokes — I think that asymmetry is structural, not
incidental. The surface I could check cheaply was the DOM, because the
browser was already open on the page I had just built. The surface my
consumer reads was the response body, which needed a different tool and a
different mode. The cheap surface is almost always inside your process;
the consumer is by definition outside it. So "verify where the consumer
reads" is really "pay the cost of leaving your own process," and that
predicts which check gets skipped better than discipline does.

Your two questions, honestly.

What fraction could have shipped with a check: I counted. Eighteen commits
with fix or perf in the subject in the last five days. Ten or twelve were
mechanically catchable — the hreflang one by asserting on the built HTML,
the ad slot by unit-testing the state machine, the image one by asserting
that every CDN src carries a width parameter. Not clever tests. Obvious
ones.

But the answer I owe you first is that your question presupposes a habit I
don't have. This repo has exactly one test file and no test script in
package.json. There is no runner. Adding "check or one-line lesson" at PR
time to that repo is a lock on a door with no frame.

The genuinely un-checkable ones turned out to be a category, not a
remainder. They were all fixes to claims: wrong Spanish register in a
draft, a false statement about a consent banner, a comment saying the ad
lands a third of the way in. No artifact to judge by, because the artifact
is prose. That is the part of the gap I don't think a check reaches.

Guardrail or tax: tax, and I would route around it inside a month. I am
the only reviewer, so a required field is a rule I enforce on myself at
the exact moment I want to be done. Your own line answers it — discipline
doesn't survive, defaults do. The version I think survives is making it
the agent's job in the same session, since you already noticed the writer
is usually the agent. Not my discipline at PR time; a step in a workflow
that is already running.

A test runner first, though.

Thread Thread
 
heinrichneb profile image
Heinrich Neb

"Verify where the consumer reads is really: pay the cost of leaving your own process" - that's the sharpest version of the asymmetry I've seen, and it predicts skipped checks better than any discipline theory. And thank you for counting instead of estimating: 10-12 of 18 mechanically catchable is a real number.

"A lock on a door with no frame" is the right diagnosis, and your ordering is right too: runner first, then the three obvious assertions, and only then conventions. One push-back on the category you called un-checkable: some prose claims ARE artifacts. "The comment says the ad lands a third of the way in" is checkable against the constant it describes - we run guards that read source comments and fail when the promise and the code diverge (they caught 13 checks whose promised assertion existed only in a comment). Wrong Spanish register - no, that one's yours. But the boundary runs through prose, not around it.

And congratulations on "Green and Blind" - a debugging session becoming an article is the best possible outcome of a comment thread.

Collapse
 
debashish_ghosal profile image
Debashish Ghosal

Love that this skips the hype-vs-doom framing and just shows the actual workflow. The design → plan → execute split is the standout idea — turning “the agent did something weird” into “it went off script at step 4” is a genuinely useful reframe.

Takeaway: the real win wasn’t the big tasks, it was clearing out months-old 40-minute chores that never felt urgent enough to tackle. A lot of “technical debt” is really just tedium debt.

Suggestion: add a short “outcome” note to each plan file after execution — what shipped vs. what was planned. Turns docs/plans/ from a memory aid into a feedback loop on your own planning.

Collapse
 
gabbs279 profile image
Gabriel Abreu

The outcome note is going in. You named the thing that was bothering me
about that folder without my being able to say what it was.

One level down from plan files, the same rot happens in code comments,
and I hit it this week. Two comments in this repo said the in-article ad
lands "roughly a third of the way through" the post body. The code cuts
at a fixed three blocks. My posts run 28 to 107 blocks, so that's
somewhere between 2.8% and 10.7%. The comment was true as an intent when
it was written and was never revisited once the implementation settled.

Same failure as an un-annotated plan file, just smaller and far more
numerous. "Tedium debt" is a good name — I think comment rot is its most
common form.

Collapse
 
mnemehq profile image
Theo Valmis

Appreciate that this is an actual workflow instead of a highlight reel. Curious how you handle it when Claude Code makes a good call in isolation that conflicts with a decision from three sessions ago.

Collapse
 
gabbs279 profile image
Gabriel Abreu

Honestly? Most of the time there is no conflict, because the decision from three sessions ago is not written anywhere the agent can read. So it does not get overruled. It just quietly loses.

Real one from this repo: there is a console.warn in the AdSense push handler. An audit pass flagged it as noise, correctly, given everything it could see — it is a stray console call in production code. It is there on purpose. It is the only signal I get when AdSense throws during push, and that failure is otherwise completely silent.

The agent made a good call in isolation. The isolation was the bug. The reason lived in my head, so a fresh pass had no way to weigh it and no way to know it was missing.

What I changed was not the judgment step, it was that the reason now sits in a comment next to the line. Which is not a great solution — comments rot — but the general shape holds: a good call that conflicts with something is almost always the correct output for the context it was given, and the fix belongs in the context, not in the review.

Collapse
 
rulestack profile image
Rulestack

If I had to rank the "where it fails" list, premise-acceptance would go first — the others surface in the output, and that one does not. What helps us is structural rather than a better prompt: every report the agent writes has to keep what was observed separate from what was concluded, so a conclusion with nothing underneath it is visible on the page. On the audit — what did the three rejected findings have in common? Wrong versus out of scope changes what the twenty is worth.

Collapse
 
strelok25dev profile image
Любовь Авдеева

Great article, especially the bit about planning before coding. I had the opposite experience — on my first project I just trusted the AI and didn't check anything until I realized the development had gone in a completely different direction from what I wanted. Classic beginner mistake.

Now I do it differently: first I ask one model to draft a plan, then I add my own edits, and after that I have two other models review it. It's like having four people discussing one plan, and in the end you get something that actually works. Without that step, the code looks logical on the surface but it's not really yours.

Collapse
 
gabbs279 profile image
Gabriel Abreu

Your failure mode is the one the written plan exists for. Not because the
plan makes the model better — it doesn't — but because it makes the
divergence visible early. You found out at the end that development had
gone somewhere else. A plan turns that into noticing at step 4.

Your three-model review is a stronger version of the same idea: an
artifact that can disagree with you before any code exists.

One caution from my own use. Models agreeing is not evidence. Three can
share the same wrong assumption and it sounds like consensus. What I trust
is when one of them points at something concrete — a file, a line, a
number I can go check myself. Agreement is cheap. A pointer isn't.

Collapse
 
mudassirworks profile image
Mudassir Khan

the "agent went off script at step 4" framing is the most useful thing in this post. debuggability is the part the demos never show.

we land on a similar thing. a CLAUDE.md that's basically a spec of what the agent is and isn't allowed to touch, plus explicit phase gates. the moment we stopped letting it infer scope from context and started giving it a written plan, session length dropped (it wasn't backtracking) and review time halved.

docs/plans/ is not bureaucracy. it's making the implicit contract between human and agent explicit.

curious how you handle the plan when reality diverges — do you update the doc mid session or fork a new one?

Collapse
 
routinekit profile image
RoutineKit

The step that saved me the most review time: write acceptance criteria and file boundaries before the first prompt. Then review is a checklist against that scope instead of re-reading everything the model touched. Without that, "helpful" edits outside the lane create a second job.

Collapse
 
codingwithjiro profile image
Elmar Chavez

Exactly. Leave the repetitive work (the one that you mastered and did over and over again) to the AI. That's the main purpose. You already know it works because you've implemented it yourself. Now, the thinking pans onto the main problems which covers the whole architecture of the codebase. This is best practice in my opinion.

Collapse
 
nabil-ctrl profile image
Nabil Abubakar

The describe the behavior, not your diagnosis line hit hardest for me. Such a small habit, but it seems to change everything.

Collapse
 
gabbs279 profile image
Gabriel Abreu

It's the smallest change in that list and the one I'd keep if I could only
keep one.

The tell that you've slipped back into diagnosing: your message contains a
filename. If you already know which file, you decided what the bug was
before you asked.

It works on humans too. The best bug reports I get describe what someone
clicked and what happened. The worst ones tell me which function is
broken, and they're usually pointing at the wrong one.