Chapter 22. Many Agents, One Machine
Several AI coding agents on one machine break on shared mutable
state: a repo checkout, a port registry file, the disk, the CPU, and a
browser tab pool, each one a mutable thing with more than one writer.
The isolation that works is one git worktree per task, cut from
origin/main, plus a check on the shared resources before I
trust any measurement taken on this box. I run multiple AI coding agents
against one Mac at the same time. That is the operating condition, not
an occasional overlap. While drafting this chapter I counted the live
sessions: pgrep -x claude | wc -l returned 6, and those are
the Claude processes alone; Codex sessions run on the same box. The load
line read
18:31 up 3:40, 1 user, load averages: 16.28 11.03 7.92.
Under ~/code sit 64 top-level directories once you exclude
the worktree siblings; 60 of them are git repos, and the other four
(INBOX, backups, handoffs, the-signal-chain.fleet) are holding pens. At
the 2026-06-10 “pull all” sweep the count was around 50 repos. The
machine gets busier every month, and each agent behaves as if it owns
the box. None of them does.
A single agent on this machine is reliable. Two agents touching one mutable thing, a checkout, a port registry, the disk itself, produce the failures that cost me real work. The ones below deleted uncommitted edits, blocked every tool call on the box at once, and came close to filing a false production regression. Each has a date on it, and each changed a rule I now run by default.
A branch switch in a shared checkout deletes uncommitted work
2026-06-10. Another live session switched
~/code/Suede-AI-App from main to its own
feature branch, feat/master-registry-consolidation, in the
middle of my task, and my uncommitted edits vanished with the switch. I
had been editing that same checkout in place. The note I wrote that day:
“two edits in, everything was gone.” No prompt, no error on my side. The
working tree moved out from under me and took the work with it.
The mechanism is dull. A checkout has one working tree and one HEAD, and I had pointed two sessions at both. The main checkout is shared mutable state, not a workspace, and a branch switch by any session wipes every other session’s uncommitted work without a warning. The second session did nothing wrong. It followed its own task to completion, which is the point: on a shared checkout, two correct agents produce one destroyed edit.
The same session, the same day, also appended its own entry, named
“registry-dev”, to ~/.claude/launch.json. The collision
surface is wider than git. Any file that more than one session writes is
contested ground.
One git worktree per agent task
One worktree per task, cut from origin/main into a
sibling directory, is the countermeasure:
git -C ~/code/Suede-AI-App worktree add \
~/code/Suede-AI-App.worktrees/<task-name> -b <branch> origin/main
cp ~/code/Suede-AI-App/frontend/.env.local \
~/code/Suede-AI-App.worktrees/<task-name>/frontend/.env.localThree choices are packed into those two commands. First, the branch
cuts from origin/main, not local main. This
Mac is a stale mirror of the machine that pushes to GitHub; on
2026-06-08 the local Suede-AI-App checkout was found 458 commits behind
origin/main, parked on an abandoned feature branch with a
paused rebase. Work built on local state inherits that drift. Second,
the worktree lands in a sibling directory,
~/code/<repo>.worktrees/<task-name>, so the
repo’s own tree stays clean and the task’s identity is readable from the
path. Third, frontend/.env.local gets copied by hand,
because it is gitignored and dev does not start without it. A fresh
worktree cut from origin arrives without any of your untracked local
files, and the env file is the one that matters.
Cutting from origin is not a habit a sync can retire. On 2026-06-10 I
ran a pull-all across the roughly 50 repos then in ~/code:
fetch plus fast-forward-only pull, one pass, and Suede-AI-App came out
clean on current main with no paused rebases left in the
main checkouts. The rule survived the cleanup anyway, because this box
drifts again the moment the other computer pushes. A one-time sync buys
a moment of accuracy, not a property of the machine, so the worktree
command keeps naming origin/main and I keep re-verifying
zero-behind before anything pushes or deploys from here.
Then the dev server gets registered in
~/.claude/launch.json on a unique port. The 2026-07-29
registry contains 41 configurations on 41 unique ports and resolves the
old notes’ disagreement: suede-agent-studio is on 3210, not 3211, and
3017 is absent. Read launch.json before picking a port,
because the file is what sessions collide on, not anyone’s summary of
it.
Here is what the convention looks like on the machine today: 52 worktrees spread across 33 sibling directories.
| Sibling directory | Worktrees |
|---|---|
Suede-AI-App.worktrees |
21 |
suede-promo.worktrees |
8 |
suede-voice.worktrees |
4 |
suede-brand-domains.worktrees |
3 |
| six repos at 2 each | 12 |
The remaining four directories hold one worktree each, and 19 of the
33 sit empty: shells left behind by prior cleanups, including
suede-agent-studio.worktrees,
agentix.worktrees, and suede-empire.worktrees.
Empty shells cost a directory entry and nothing else, so I leave them.
The 21 under Suede-AI-App.worktrees are the number that
tells the story. The monorepo is the busiest surface on the machine,
which is why the shared-checkout collision happened there first.
The discipline has a passive half too. As I write this, the main
Suede-AI-App checkout carries 10 uncommitted entries, two modified
fastlane metadata files plus eight untracked paths, and the
suede-agent-studio checkout is parked on a detached HEAD with a modified
src/app/landing.css and an untracked fastlane/
directory. That second one is the fragile case: detached HEAD plus
uncommitted changes means the work is anchored to no branch, and one
checkout by any session could strand it. I touch none of it. Dirty state
I did not create is presumed to be another session’s in-flight work, and
the rule is to preserve it as found rather than stashing, resetting, or
tidying it into a commit it never asked for. The paused rebase found on
2026-06-08 got the same treatment, and the pull-all sweep left four
broken-remote snapshots under ~/from-jasons-macbook
untouched on the same reasoning. Cleaning up someone else’s working tree
on this machine is deletion.
When another session fills the disk: ENOSPC on every command
2026-07-22. Every Bash call failed at once with
ENOSPC: no space left on device. Not most calls: every
call, including df -h, the command whose whole job is to
diagnose the condition. File writes failed in the scratchpad and inside
the repo alike. When the tool that measures free space cannot run for
lack of free space, the failure reads like a broken harness or a revoked
permission. It was neither. Other concurrent sessions’ worktrees,
builds, and node_modules trees had eaten the machine to zero.
Then it healed on its own. Free space went from 0 to 4-5GB within 1-2
minutes as another session’s build or install finished. That produced
two rules. On suspected disk exhaustion, wait a short interval and retry
a plain df -h / before concluding anything is
unrecoverable, because the condition is often transient on a box where
other agents are mid-build. And never attempt blind cleanup. With Bash
down you cannot ls or du to learn what is safe
to remove, so any deletion is a guess, and the thing you guess wrong
about is another session’s work in progress.
Reading df on this Mac takes care even when it runs.
df -h / reports the sealed read-only system volume,
/dev/disk1s1s1: 466Gi size, 12Gi used, 11% capacity. That
looks like an empty disk. The volume doing the work is
/System/Volumes/Data, /dev/disk1s2: 342Gi
used, 78% capacity, sharing the same 99Gi available pool. Budget from
the Data volume or from the shared available figure, or the 11% will lie
to you at the worst moment.
Free-space math has a second trap: peak versus result. The
Suede-AI-App/frontend install weighs 3.1GB+ across 1173
top-level entries, most of it a heavy web3/wallet SDK dependency tree.
On 2026-07-22 that install failed with ENOSPC while df
showed a few GB free, because npm’s tarball-extraction peak exceeds the
final installed size. Headroom that covers the result does not cover the
build. Budget for the peak.
Cloning node_modules with cp -Rc instead of installing
If another worktree, or the main checkout, has a working
node_modules built from an identical
package-lock.json, clone it instead of installing. The same
day produced that workaround, and it now carries most of the disk
pressure here:
diff ~/code/Suede-AI-App/frontend/package-lock.json ./package-lock.json # must be identical
cp -Rc ~/code/Suede-AI-App/frontend/node_modules ./node_modulesThe -c flag asks macOS for APFS
clonefile(), copy-on-write cloning. Measured on the 3.1GB,
1173-entry tree: df stayed flat within 0.1GB across the
entire clone. Blocks get duplicated on disk when a file is modified
later, and until then a multi-gigabyte dependency tree costs metadata.
On a machine that has hit ENOSPC from sibling sessions, an install that
consumes no space beats an install that races the neighbors for it. The
neighbors are also what make the workaround possible: on a box carrying
52 worktrees, the odds are good that some sibling has paid for the
install you need, and the densest repo, the one whose 3.1GB tree causes
the pressure, is the one most certain to have a donor sitting next
door.
The lockfile check is not optional. The clone reproduces the source
tree byte for byte, so if the lockfiles diverge you get a
node_modules that matches neither manifest, and the failure
shows up later as a runtime mystery instead of an install error. Diff
first, then clone.
One more calibration from the same day: free bytes do not mean free
time. A 1173-entry tree is minutes of metadata work even when no data
moves. If a cp -Rc runs past about 180 seconds, move it to
the background rather than concluding it hung.
Why a load average of 838 invalidates a benchmark
CPU is the other contended resource, and its failure mode is quieter than the disk’s because nothing errors. The numbers come back wrong instead.
During an estate-wide Lighthouse performance pass, load on this machine hit 838, driven by other concurrent sessions, not by the pages under test. A performance score measured under load 838 describes the contention, not the code. Ship a fix based on that score and you are tuning a site against a traffic jam it will never see again.
The countermeasure costs one command: run uptime before
trusting any measurement taken on this box, and read the whole triple,
not the first number. The snapshot at the top of this chapter, 16.28
then 11.03 then 7.92, says more than “the machine is busy.” The 1-minute
average at double the 15-minute average means concurrent work ramped up
inside the last few minutes. A benchmark started in that window lands in
the worst of the ramp and reads as a regression that does not exist. The
same logic covers flaky tests: a suite that fails at load 16 and passes
at load 2 measured the machine, not the code.
Shared browser tabs return another session’s page
A shared verification tool is the subtlest collision on this machine, because a wrong verification result converts a healthy system into a false incident.
2026-07-17, while verifying a live page through a shared
browser-automation tab pool: the tool returned stale content from a
different origin than the one I had navigated to. Concurrent agents
reuse and overwrite tabs in the pool, so a shared tab’s state proves
nothing about the page you think you are on. Taken at face value, that
stale readout was minutes away from being filed as a production
regression. The rule that came out of it: open a dedicated tab with
tabs_create and re-verify there before concluding anything.
On this machine, a suspicious browser result is a tooling signal first
and a finding second.
The sharpest version came earlier, on 2026-06-11, during the settlement-gate incident. While I repaired the x402 settlement default from an isolated worktree, a concurrent session counter-edited the same defaults in the main checkout. Two of the post-mortem rules exist because of that overlap: one orchestrator per repo at a time, and when sessions do overlap, trust commit contents, not commit messages. A message records an intention. On a contested repo, the diff is the testimony.
Shared surfaces, collisions, and countermeasures
| Shared surface | Collision | Countermeasure |
|---|---|---|
| Repo checkout | Branch switch wiped uncommitted edits (2026-06-10) | One worktree per task, cut from origin/main |
frontend/.env.local |
Fresh worktree cannot start dev | Hand-copy from the main checkout |
~/.claude/launch.json |
Concurrent entries, contested ports | Unique port per session; read the file before writing it |
| Disk | ENOSPC took down every tool call (2026-07-22) | Wait and retry df -h /; no blind cleanup;
cp -Rc instead of install |
| CPU | Load 838 under a benchmark pass | uptime before trusting any measurement |
| Browser tab pool | Stale cross-origin readout (2026-07-17) | Dedicated tab per verification |
Every row is the same shape: one mutable thing, two writers, and a failure that arrived without an error message. The checkout collision deleted work in silence. The stale tab reported wrong data with full confidence. The load average corrupted a score that looked precise to two decimal places. Shared-state failures on this machine do not announce themselves, which is why the countermeasures run before the evidence of trouble, not after.
The 2026-06-10 note flags that the same caution “likely applies” to other busy repos on the machine, marked as likely rather than confirmed. I run it as confirmed anyway. The collision mechanism has nothing repo-specific in it, and the price of being wrong in the cautious direction is one extra directory.
The numbers say which way this is trending. Around 50 repos at the 2026-06-10 sweep, 60 now, plus 52 worktrees and 6 concurrent sessions on the evening I checked. The fleet grows and the machine does not. What holds it together is that each session’s blast radius stays inside a directory with the task’s name on it, and everything the sessions cannot avoid sharing, the disk, the CPU, the port file, the browser, gets measured before it gets trusted.