How we actually build apps with Claude
Not "AI wrote my app". The working setup - detached terminal sessions per product, a mode-plus-project session contract, six guard hooks that refuse things, MCP into our own products, and the deploy and telemetry path the agent has to survive - on an estate that was already in production first.
Most writing about coding agents describes starting something. This describes operating something that already exists: 38 shared Go libraries, 75 modules, a dozen applications in production, one person.
That order matters. The estate was large, deployed and load-bearing before any agent was pointed at it, so nothing here is a greenfield demo and none of it gets to assume a clean slate. What follows is the current setup, measured on 17 August 2026. It is not the story of how it got this way - each of those is its own piece.
It took a while to arrive at, but the useful unit isn't the prompt. It is everything around the prompt: where the session runs, what it knows before you type, what it's physically prevented from doing, and what happens to its output afterwards.
Where the work happens
Not on a laptop. Sessions run on a machine I connect to with mosh, and each connection lands in a named tmux session:
mosh <host> -- tmux new -A -s haven
-A means attach if it exists, create if it doesn't. So the connection is
disposable and the session isn't. Close the laptop, open it on a train,
reattach, and the agent has been working the whole time. A dropped connection
costs you nothing, which matters when a single run can go for an hour.
There is one tmux session per product and one window per concern. Right now that is 10 sessions and 44 windows, 21 of them running an agent. A typical product has a shell window, a window running a dev session, another running a planning session, and one for whatever is on fire.
Those windows are addressable. 56 sessions are reachable by name - the local ones, plus sessions attached over Remote Control from a phone, plus cloud ones. They can send each other messages. The handover that produced part of this note was one session asking another what state it had left some drafts in, because that session had the context and re-deriving it would have cost an hour.
Between 12 July and 17 August 2026 - 37 active days - that setup produced 150 main session transcripts and 989 subagent transcripts, 58,351 tool calls and 119 cross-session messages. Those messages are the cheapest way I've found to stop two agents editing the same working tree, which they'll otherwise do.
The tool breakdown is the part I'd look at first:
| tool | calls | share |
|---|---|---|
| Bash | 35,283 | 60% |
| Edit | 6,631 | 11% |
| Read | 3,971 | 7% |
| Write | 1,871 | 3% |
Six in ten actions are a shell command. Editing and writing files together come to 14%. Most of what the agent does is run things, read the output and check state, which is roughly what the job is when the code already exists.
A session is a mode and a project
A session opens with a slash command:
/tam:dev kythene
/tam:qa acheev
/tam:plan haven
/tam:mktg tam
The mode is the command and the project is the argument. There are 13 modes and 9 project files, and they compose: the mode says what kind of work this is (build the tickets I name; review and raise tickets, don't fix; design and ticket, don't implement; marketing surfaces only, no app code), and the project file supplies the estate facts - repos, URLs, dev ports, the deployment path, the conventions that app breaks.
That is a checked-in file rather than a paragraph retyped every morning, which
is the whole trick. The /tam:mktg mode's rule about never touching app code is
enforced on every marketing session because it is written down once, not because
I remembered to say it.
Underneath sit 25 skills - packaged procedures rather than prompts. Database patterns, htmx patterns, module layout, changelog entries, commit style, ticketing, deployment, UI review, testing, releases. A skill is loaded when the work matches it, so the session that's about to write a migration gets the migration rules without me pasting them.
In the same period, the most-loaded were ship (24), bump-deps (19),
house-voice (16) and tickets (14). Those are the boring repeated
procedures, which is exactly what you would want to be automating.
Guards, because instructions are advisory
This is the part I would keep if I had to throw the rest away.
An instruction is a suggestion with good intentions. A hook is a script that runs before the tool call and can refuse it. There are six, and every one exists because something went wrong once.
One number makes the case better than any argument. The rule "never
pkill/killall/group-kill a dev process" is written down in three separate
skill files, in capital letters, with the reason attached each time. Across
those 37 days the hook enforcing it refused a command 46 times, in 37
different sessions.
So the instruction was clear, repeated, justified and loaded into every one of those sessions, and the thing it prohibits was still attempted 1.2 times per active day. Whatever an instruction is, it isn't a control.
The incident behind it: on 7 June 2026 a kill -KILL aimed at a dev server's
process group climbed the process tree and took down the whole desktop session.
The hook now permits kill <pid> and pkill <exact-name>, and denies
killall, group targets and negative PIDs. When it denies, it says what to do
instead:
To kill a dev server, target an explicit positive PID from
ps, or isolate it:systemd-run --user --scope -u <app>-dev <cmd>thensystemctl --user stop <app>-dev.
That last part is a design rule I'd hold to rather than a measured finding: a denial that only refuses leaves the caller to invent a way round it, and a denial carrying the correct move usually gets that move next.
The second one is worth showing properly, because it's the shape most people
will need. ui/assets/css/main.css and ui/assets/main.js are build outputs
tracked in git and embedded in the binary, and CI never builds them. So a
commit that changes a template without rebuilding them ships stale CSS - silently,
and it renders fine locally where the dev server rebuilds continuously.
The hook fires on git commit, and only when it has to:
# Only git commits, and not when explicitly bypassed.
printf '%s' "$cmd" | grep -qP '\bgit\b[^|;&]*\bcommit\b' || exit 0
printf '%s' "$cmd" | grep -q 'ASSETS_OK=1' && exit 0
# Only apps with a Tailwind/esbuild build have committed assets to keep fresh.
[ -d "$root/ui/tailwind" ] || exit 0
# Nothing that can affect the Tailwind/esbuild output: let it through.
printf '%s\n' "$changed" | grep -qE '^(ui/|go\.mod$|go\.sum$)' || exit 0
Then it runs the build - which is deterministic and sub-second, so an up-to-date tree costs nothing - and blocks only if the rebuild produced something the commit doesn't carry:
stale="$( { git -C "$root" diff --name-only -- ui/assets; \
git -C "$root" ls-files --others --exclude-standard -- ui/assets; } \
2>/dev/null | sort -u)"
[ -z "$stale" ] && exit 0
deny "Blocked: ui/assets was stale for this commit. These are tracked BUILD
OUTPUTS embedded in the binary, and CI never rebuilds them - committing without
them ships stale CSS/JS.
The rebuild has already been run for you (\`tam assets --build\`) and updated:
$stale
\`git add\` those into this commit and re-run. Do not make a separate rebuild
commit."
Three properties make it work, and they generalise to any guard worth writing:
- It does the fix. By the time you read the denial the rebuild has already
run. The remaining action is
git add. - It has an escape hatch you have to type.
ASSETS_OK=1gets past it. It guards against momentum rather than malice, and a guard with no way through gets disabled the first time it's wrong. - It exits early and often. Wrong repo, no build directory, nothing relevant staged - out. A guard that costs something on every unrelated commit gets removed within a week.
That one has blocked commits in two sessions, on 8, 11 and 15 August. A low number is the correct outcome for a guard whose job is to catch a rare and expensive mistake.
The other four: guard-git denies --no-verify and denies amending a commit
that's already pushed. guard-migration-edit blocks editing a migration that is
tracked in git, on the grounds that a tracked migration has been applied
somewhere, and pushes you to add a new one. guard-release blocks cutting a
release on the apps that ship only when asked, because the natural end of a good
session is momentum towards deploying and that's expensive. content-guard is
the odd one out and is deliberately non-blocking - it injects reminders after a
write, red squigglies rather than a refusal.
The dev loop the agent actually drives
Each product has a process-compose fleet, started detached:
tam dev up
tam dev status
tam dev logs <proc>
tam dev restart <proc>
Each process is a tam devserver, which holds a per-app lock, runs the generate
step, and restarts the binary when files change. process-compose adds the fleet
layer on top: detached operation, per-process log capture, readiness probes on
each app's /health.
The lock matters more than it sounds. Restart policy is on_failure rather than
always, so a developer running make dev in a terminal takes the lock from the
fleet and the fleet's copy exits cleanly instead of two supervisors fighting over
one port. That failure mode cost an afternoon before it was designed out.
On top of that the agent drives a real browser - Chromium over MCP - against the running app. It navigates, clicks, fills forms, reads the console and takes screenshots. In the period that was 805 navigations, 639 page evaluations, 528 screenshots and 173 console reads.
This is not optional, and the reason is in the failures section below.
From a commit to something running
Deployment is config-as-code in one repository, and nothing else. No prod/
directory inside an app repo, and no editing a machine by hand - both of those
produce an estate whose actual state exists only in someone's memory.
The path is: commit and push to a git server we run ourselves (OneDev), which runs the build-and-test gate on every push. Cutting a release is a version bump plus a tag; the tag triggers a CI job that vendors dependencies, builds a container image and pushes it. Deploying is a separate, explicit command that takes a target and a stack, run from the config repository.
Cut and deploy stay separate on purpose. An image existing isn't the same decision as that image serving traffic, and collapsing the two is how you end up shipping on a Friday because the tests happened to pass.
Seeing what happened
Two lenses, deliberately not merged.
Machines: everything speaks OTLP to a collector, which fans out to VictoriaMetrics for metrics, VictoriaLogs for logs and VictoriaTraces for traces, with Grafana over the top. That is health, errors, bots and scanners.
Humans: PostHog, separately, for what people actually did.
Merging them puts every bot sweep and uptime probe into your product analytics, and the number that tells you whether a feature is used stops meaning anything.
The agent queries both directly rather than through a dashboard, which is the part that changed how debugging feels - the loop is "what does production actually say" rather than "which panel might show this".
And one thing deliberately outside all of it: Better Stack, for uptime and incident alerting. Everything above runs on infrastructure we manage, which means every alert path above shares a fate with the thing it is watching - the vmalert/alertmanager/ntfy chain cannot page you about the machine it lives on. Better Stack is external, so it is the one component that still works when the answer is "the whole stack is down". That is the same argument as the dead man's switch, applied to the alerting itself rather than to a single job.
Where the state that isn't code lives
Code is in git. Everything that's not code - decisions, findings, the reasoning behind a choice, measured facts and when they were measured - goes into Kythene, and sessions recall from it rather than being briefed.
This note is being reviewed there as it's written, which is also how the writing loop works: the markdown is canonical in git, each round is published as a collection with review switched on, comments come back block by block from a phone, and the markdown gets a new revision. The relationship is the one a branch has to a pull request.
The saving discipline is the load-bearing part, and it is one line: when a claim is verified against the live system, save the figure, the date and the method. A later session can then cite it without re-deriving it, and a claim that turns out to be wrong can be traced to when it was true. The next section is what happens when you skip that.
What doesn't work
It will repeat an unverified number from its own notes, confidently. A draft about replacing an observability stack carried "roughly a tenth of the memory" for a day, sourced from an internal note nobody had checked. It was wrong, and it was in a post about engineering competence. The stack it was comparing against had been deleted, so the original figure was unrecoverable, and that paragraph now says the number does not exist rather than giving one.
Verification has to be a step somebody performs, not a property you hope the output has. And the note that started it's still in the knowledge store saying "~1/10th the RAM", which is the actual lesson: a wrong fact you wrote down propagates further than a wrong fact you said.
DOM assertions prove elements exist, not that anything rendered. A passing
elementFromPoint check once shipped a content area with zero height to
production. The elements were all present, correctly nested, and occupying no
space. Screenshot the page, or you don't know what it looks like - which is why
528 of them appear in the tool counts above.
Emulating a narrow viewport isn't a phone. Resizing the browser gives you a narrow desktop: device pixel ratio 1, no touch events, no browser chrome consuming vertical space. Mobile problems reproduce under real device emulation or not at all, and sometimes not even then, at which point the answer is to ask a human with a phone.
It can't tell you what is worth building. Every judgement in this estate about what to do next has been a human one, and the times I've let a plausible agent-generated roadmap stand have produced work that was well made and pointless. It is very good at "here is how that would be built" and has no view at all on "should it be".
Two sessions in one working tree collide. Which is mundane, and led to a rule that turned out to have a better reason than the one it was written for: one session designs and raises tickets, a different session builds them. The mundane reason is file conflicts. The real reason is that a reviewer who wrote the thing is not a reviewer, and a session that plans and implements in one breath produces work that agrees with itself all the way to production.
Cost is real work, and it belongs in the plan rather than the footnotes. 58,351 tool calls isn't free, and scoping a session so it doesn't read the whole repository to answer a small question is a skill that takes a while to acquire.
What of this transfers
Does any of this survive a team?
My instinct was that the guard hooks would transfer and the session contract would not, on the grounds that the contract assumes one person agrees with all of it. I think that is wrong.
All of it transfers, and most of it transfers better. The guards are policy-as-code, and a team of thirty is exactly where policy-as-code stops being a nicety - a rule nobody can quietly skip is worth far more when there are thirty people who might. The session contract is a naming convention plus a scope, which is the same kind of artefact as a branch-naming standard or a service catalogue, and organisations run those fine. The knowledge conventions are the ones that improve with people in them: a shared store that several engineers write to is more useful than one person's, not less.
What changes is not whether it works but who owns it. At one person the conventions live in your head and in a plugin directory. At thirty they need an owner, a review process for changes to them, and somewhere they are written down that is not a session transcript. That is a platform team's job, and the work is the governance rather than the mechanism.
What the actual work was
Very little of the above is about the agent.
The skills, the project files, the guards and the knowledge store are all the same activity: writing down how this estate works in enough detail that a competent stranger could work in it correctly. The agent is that stranger. It arrives fresh every single time, it never gets bored of the conventions, and it never once decides the rule doesn't apply today.
Which means the work was documentation, done under a deadline that finally mattered, and the payoff has been unevenly distributed towards the parts I wrote down most carefully. That isn't a satisfying conclusion for anyone hoping the tool was the answer, but it's what the last two months actually looked like.
If you're running something comparable and your guards catch things mine do not, I would like to see them - the whole set here came from incidents, and incidents are a slow way to learn.