I run one production codebase, three client projects, and a handful of internal tools with agentic help, and there is no platform team behind me. No one reviews my MCP server sprawl in a design doc, no one notices when a session quietly burns 40,000 tokens loading tool descriptions it never uses, and no one catches it when a long session starts confidently hallucinating a decision I never made three files ago. If any of that goes wrong, I find out from a broken build, not from a Slack thread where someone already flagged it.
For a long time I treated that as a disadvantage. It isn’t, not entirely. A platform team can paper over waste with headcount and budget. I can’t, so every piece of my setup has to earn its place. What’s left after eighteen months of cutting is a small, boring, mechanical system I now think is stronger than what a lot of better-resourced teams run, precisely because nobody let me get away with sloppiness.
This is a walkthrough of that system as it exists today: the instruction file, the permission and hook setup, the startup sequence before touching real work, when I reach for parallel and pipeline execution instead of a plain loop, and the two cost levers that decide whether my monthly bill is sane. Real file contents, not descriptions of them. If something below looks copy-paste ready, it’s because I copy-pasted it out of my own repo.
The two numbers that justify all of this
Before the harness, two numbers changed how I think about agent design.
The first is from Anthropic’s own writeup on code execution with MCP servers. They walk through a concrete workflow: an agent reads a meeting transcript out of Google Drive and attaches it to a Salesforce lead. Done the naive way, every tool definition loaded into context and the full transcript passing through the model twice, once in and once out, that workflow costs about 150,000 tokens. Done with code execution, where the agent writes and runs code against the APIs instead of routing every intermediate result through its own context, the same workflow costs about 2,000 tokens. That’s a 98.7 percent reduction, and it isn’t a cherry-picked benchmark, it’s what happens when you stop treating the context window as free scratch space for data the model never needed to read.
I don’t run a fleet of MCP servers, but the lesson generalizes: every tool description, every schema, every piece of context you load “just in case” is a tax paid on every single turn, not a one-time cost. A solo operator feels that tax directly in the bill in a way a team billing to a shared cost center often doesn’t notice until finance asks a question.
The second number is Chroma’s Context Rot research, which ran eighteen models, Claude, GPT, Gemini, and Qwen families included, through controlled long-context tasks. The uncomfortable finding is that near-perfect scores on needle-in-a-haystack benchmarks do not predict stable performance on realistic tasks. Every model tested showed a measurable gap between a focused prompt, around 300 tokens of exactly what’s needed, and the same question inside a much longer prompt, on the order of 113,000 tokens of mostly-irrelevant material. Even a single distractor sentence dropped into a long context measurably hurt accuracy, and the effect got worse as context grew, even though the correct answer was equally present every time.
Put those two findings together and you get the actual design constraint behind everything that follows: length is not neutral. Every token you load costs money on the way in and accuracy on the way out. A well-funded team can throw compute at that problem and mostly not feel it. I can’t, and it turns out not feeling it was never actually free, it was just being paid for by someone else’s budget.
CLAUDE.md: what actually earns its place in the instruction file
My CLAUDE.md is not a style guide. Style guides are cheap to write and mostly ignored under pressure, the same way a human under deadline forgets the linting rules nobody enforces mechanically. What’s in mine is the stuff that has actually cost me time when it was missing.
# CLAUDE.md
## Definition of done, not a feeling
Never declare a task complete based on your own read of the diff.
Done means:
1. `npm test` exits 0. Paste the exit code, not a paraphrase.
2. Every file in PLAN.md's checklist shows as modified in
`git diff --stat`.
3. `npm run typecheck` exits 0.
If any of these is false, say so and keep working. Do not summarize
partial progress as if it were completion.
## Files you may read but never edit
- SPEC.md: the contract for the current task. If you think it's
wrong, write the objection to DECISIONS.md and stop for review.
## Stop conditions
- Same failing command, same error, three times in a row: stop,
write the failure to DECISIONS.md, do not attempt a fourth time.
- No measurable checklist progress across two consecutive turns:
stop, the plan is probably wrong, not the execution.
## How this repo actually works
- Package manager is pnpm, not npm, despite what half the old READMEs
still say. Using npm here has silently created a second lockfile
twice.
- The `legacy/` directory is read-only reference code from before the
rewrite. It is not dead code to clean up, it is there on purpose
for people migrating integrations off the old API.
- Tests that hit the payments provider run against a sandbox key by
default. If SANDBOX_KEY is unset, do not fall back to a real key,
stop and ask.
## What I actually want from you
Tell me when you're not sure, in the response itself. A wrong guess
stated confidently costs me more than an honest "I'm not sure whether
X or Y is correct here, I picked X because ___."
Notice what’s missing: no tone guidance, no “be concise,” no restating of things the model already does by default. Every line exists because a specific failure happened once and I decided it was cheap to prevent forever. The “package manager is pnpm” line looks trivial until you’ve watched an agent confidently npm install its way into a second lockfile that silently drifts from the real one, twice, months apart, because nothing in the repo told it not to.
The stop conditions do double duty. They stop wasted turns, and per the context rot numbers above, they stop an agent from padding its own context with three more attempts at the same failing command, each of which is exactly the kind of low-value filler that degrades everything read after it.
Permissions and hooks: constraints enforced by code, not by asking nicely
Prose instructions in CLAUDE.md are read, mostly followed, and occasionally skipped once a session is deep enough into a task that the instruction has scrolled out of active attention. That’s not a criticism of the model, it’s what happens with any instruction that lives as text rather than a mechanical gate. The things that must never happen belong in .claude/settings.json, enforced by a hook script that runs whether or not the model remembers the rule.
Here’s the permission block and one hook from my actual setup:
{
"permissions": {
"allow": [
"Bash(npm test*)",
"Bash(npm run lint*)",
"Bash(git diff*)",
"Bash(git status*)",
"Edit(src/**)",
"Read(**)"
],
"deny": [
"Bash(git push --force*)",
"Bash(rm -rf*)",
"Edit(billing/pricing-table.json)",
"Edit(.env*)",
"Read(.env*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git push*)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-branch.sh"
}
]
}
]
}
}
check-branch.sh is the piece that actually matters, because "don't push to main" in CLAUDE.md is a suggestion, and this is not:
#!/bin/bash
# .claude/hooks/check-branch.sh
COMMAND=$(jq -r '.tool_input.command')
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if echo "$COMMAND" | grep -q 'git push' && ["$CURRENT_BRANCH" = "main"]; then
echo "Blocked: direct push to main is not allowed. Open a branch." >&2
exit 2
fi
exit 0
Exit code 2 is a hard block, the tool call never runs, and the model gets the stderr message back as the reason. That’s the difference between “an agent that was told not to push to main and mostly didn’t” and “an agent that structurally cannot push to main,” and after running unattended sessions for a while, that distinction stopped feeling like a nice-to-have. The .env deny rules exist for the same reason a junior engineer doesn't get production credentials on day one: not distrust of judgment specifically, just a blast radius too large to leave judgment as the only gate.
One habit worth naming: I keep the allow list narrower than feels comfortable at first, and widen it only when a real task gets blocked by something that should have been fine. It’s cheaper to notice “I need to allow Bash(docker compose*) now" than to notice, after the fact, that the list was wide enough to let something destructive through on a session I wasn't watching.
Phase 0: what happens before any real work starts
Every new session, before it touches a single file, runs the same short sequence. I call it Phase 0, and it exists because the single most expensive mistake I’ve made with agents was letting a session start “helping” before it had actually read the state it was walking into.
Roughly what that looks like in practice, from an actual session start:
> Before doing anything else, read PROGRESS.md, SPEC.md, and run
> `git status` and `git log --oneline -10`. Tell me what state this
> repo is in and what you think the next step is. Do not start
> editing anything yet.
I read PROGRESS.md, SPEC.md, and checked git status and recent log.
Current state:
- PROGRESS.md says step 3 of 6 is in progress: "backfill running,
last checkpoint at row 214,000 of 1,340,000."
- git status shows a clean tree, no uncommitted changes.
- Last commit (2 hours ago): "wip: batch backfill job, size 5000"
- SPEC.md requires zero downtime and reversibility; nothing in the
current diff conflicts with that as far as I can see.
One thing I want to confirm before continuing: PROGRESS.md says the
backfill batch size was dropped from 50,000 to 5,000 after a
replication lag alert. I don't see that alert or its resolution
logged in DECISIONS.md. Should I proceed assuming 5,000 is still the
right batch size, or is there newer guidance I'm missing?
I have not made any edits yet.
That last paragraph is the entire point of Phase 0. The session found a gap between what PROGRESS.md claimed and what DECISIONS.md recorded, and stopped to ask instead of guessing. That costs me fifteen seconds of reading. A session that silently assumes 50,000 is fine because it seems more efficient has cost me a multi-hour incident before, and I only need to learn that lesson once if I actually encode the fix.
The mechanical version of Phase 0 lives in a SessionStart hook that fires the read-only recon automatically instead of relying on me typing it every time:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/phase0-brief.sh"
}
]
}
]
}
}
phase0-brief.sh just cats PROGRESS.md, SPEC.md, and the last few commits into the session's initial context as additionalContext, so the model gets the brief without spending a tool call to fetch it. Small thing, but it means Phase 0 happens even on sessions I kick off from my phone between meetings, when I'm not going to type the recon prompt myself.
Graph and workflow primitives: when a loop stops being enough
For most of what I do, a single agent loop, one conversation, tools called as needed, is the right tool. It’s the cheapest to reason about, the cheapest to debug, and the cheapest in tokens because there’s exactly one context accumulating cost. My early mistake was reaching for orchestration before I needed it, building a multi-agent pipeline for a task a loop with a good CLAUDE.md would have handled in a third of the tokens.
The test I use now: promote a loop to a workflow only when the task has genuinely independent sub-parts that don’t need to see each other’s intermediate reasoning, or when different parts of the task justify genuinely different models. If one context can do the whole thing coherently, keep it one context.
Claude Code’s workflow runtime, the part most people never open, lives as plain code under .claude/workflows/, not a YAML config or a separate framework. That matters more than it sounds: the orchestration is code you write and can read a year later, not a black box a framework owns for you. A pipeline stage looks like this:
// .claude/workflows/review-batch.js
export const meta = {
name: "review-batch",
description: "Triage and review a batch of open PRs",
};
export default async function ({ pipeline, parallel, agent }) {
const prs = await fetchOpenPRs();
// pipeline(): no PR waits on another to finish triage.
const triaged = await pipeline(
prs,
(pr) => agent(`Triage PR #${pr.number}: is this trivial, medium, \
or needs deep review? Reply with one word.`, {
model: "claude-haiku-4-5",
label: `triage:${pr.number}`,
}),
);
const needsDeepReview = triaged.filter((t) => t.result === "deep");
// parallel(): a barrier, not a streaming pipeline, because I want
// the whole batch done before I read any of it.
const reviews = await parallel(
needsDeepReview.map((pr) => () =>
agent(`Do a full review of PR #${pr.pr.number}, focus on \
correctness and security.`, {
model: "claude-opus-4-6",
label: `review:${pr.pr.number}`,
})
),
);
return { triaged, reviews };
}
Two decisions in that file change the cost and the shape of the result. Pipeline over parallel for triage, since results don’t depend on each other and the fast ones shouldn’t wait on the slowest. Parallel for deep review, a real barrier, because I want the whole batch done before reading any of it. Default to pipeline, reach for the barrier only when a step needs every prior result at once; getting that backwards doesn’t break anything, it just makes the run slower for no reason, an easy mistake to make once and never notice.
The other decision, model assignment per node, is the second cost lever, worth its own section.
The two cost levers nobody budgets for
Lever one: orchestration is free, model calls are not. The control flow in that workflow file, the pipeline() and parallel() calls, the filtering, the fetching of open PRs, all of that runs as ordinary code. It costs zero model tokens, because it's code executing on my machine, not a conversation with a model deciding what to do next. Only the agent() calls cost tokens. A solo operator who doesn't internalize this ends up doing coordination inside a model conversation, asking the agent to plan out which PRs need deep review, paying tokens for work a for-loop does free and deterministically besides. Write the parts of the process that have a definite shape as code, and reserve model calls for the parts that actually require judgment.
Lever two: not every node needs your best model. In the workflow above, triage runs on a Haiku-class model, one word of output, well inside what a small model handles reliably. Deep review runs on an Opus-class model, because getting a security review wrong is the expensive failure mode, not the token cost of the call. That split is usually where most of the money in a multi-step pipeline actually goes, because the temptation when a workflow first ships is to leave every node on whatever model the session happens to be running. Before a large run I check /model node by node and ask whether this step's failure mode justifies the expensive model, or whether I'm paying premium rates for a one-word classification. Most of the time the honest answer is no for at least half the graph.
Put together, these two levers are why a five-node workflow can cost less than a single long loop on the same task, and why a badly designed one, paying model reasoning for coordination a for-loop should do, or defaulting every node to the expensive model, can cost more than the loop it replaced. The primitive isn’t automatically cheaper. Using it correctly is.
My actual week
None of this is theoretical, it’s what my week looks like, and the honest version is more useful than a clean-sounding summary.
Monday morning is the only time I sit down and actually read, not skim, the DECISIONS.md files across active projects from the previous week. Fifteen, twenty minutes. This is the one part I don’t think can be automated away, because the value of a human in the loop is judgment about which past decisions are still right, and that requires actually reading them.
Most of the coding work during the week runs as plain agent loops: kicked off, checked on twice, verified in a fresh /clear session before I trust the result. That's still the majority of what I do, because most tasks genuinely are one coherent piece of work, not a graph.
Workflows run unattended overnight for the genuinely parallel work, or work that benefits from model-per-node routing: the PR triage batch above, a nightly dependency-audit pipeline, a documentation-drift check that fans out across every service repo and flags where the README has drifted from the actual API surface. I wake up to a summary, not a pile of raw output, because the workflow’s last step is producing that summary, not me reading six sessions’ worth of transcript.
Friday afternoon is a deliberate cost review, npx ccusage@latest weekly or the equivalent, sorted by session and by workflow run, looking for the two patterns that have burned me before: a session that ran far more turns than the task justified, and a node that quietly ran on a bigger model than it needed. Both show up as outliers in five minutes, if you look.
How I actually know this is working
Activity is easy to produce and easy to mistake for progress. Turns run, tokens burn, commits land, and none of that tells you whether the system is holding up. The signal I trust is narrower.
First: how often do I intervene mid-task versus after the fact. A working system produces fewer mid-task rescues over time, not more sessions completed. If I’m still babysitting every long-running task the way I was six months ago, the harness isn’t doing its job.
Second: what DECISIONS.md looks like across a month. A healthy log has specific entries and occasionally an honest objection, a session that flagged a spec conflict instead of quietly resolving it. A log that’s suspiciously smooth, every entry a clean confirmation, usually means verification has gotten lazy, not that the work got easier.
Third: the ccusage session view, sorted by cost, on a bad week versus a good one. On a bad week the top is dominated by one or two sessions that ran away, 200-plus turns, most adding nothing. On a good week cost is spread evenly across genuinely useful work, and nothing at the top surprises me.
None of those three checks require a dashboard or a platform team, just the harness already built. That’s the actual point of building it this way. The discipline a solo operator is forced into by not having a safety net turns out to be the same discipline that makes the system worth trusting, and the parts that felt like overhead in month one are the parts I’d tell anyone building this alone to do first.
Tags: claude-code, ai-agents, developer-tools, software-engineering, llm-cost-optimization, devops, solo-developer
Top comments (0)