DEV Community

Cover image for I run 5 Claude Code CLIs from one control plane. Here's the plumbing.
Ran Levi
Ran Levi

Posted on

I run 5 Claude Code CLIs from one control plane. Here's the plumbing.

Claude Code is great at being one agent in one terminal on one repo. The trouble starts at
agent number three. I had five projects going, five terminal windows, and every morning I'd
sit down and spend ten minutes just working out which agent was mid-task, which was blocked
waiting on me, and which had quietly finished an hour ago.

The thing nobody tells you about running multiple agents is that you become the message
bus.
Every agent routes through your attention. Scale that to five and the agents aren't
the bottleneck — you are. So I built a small control plane to be the message bus instead of
me. This is how it's wired, including the parts that fought back.

Constraint that shaped everything: drive the CLI, not the API

The obvious move is to hit Anthropic's API directly and build your own agent loop. I
deliberately didn't. Two reasons.

First, cost and trust: driving the claude CLI means the work runs on your existing Claude
subscription, not a metered API key. It costs exactly what Claude Code costs you today, and
nothing routes through a server I run. For a tool people install on their own machine, "your
subscription, your keys, your data" isn't a feature — it's the whole deal.

Second, the CLI already is the agent. It has the tool-use loop, the permission model, the
file editing, the MCP support. Re-implementing that against the raw API is throwing away the
best part. So the design flipped: instead of being the agent, the control plane spawns
and supervises claude CLI processes
and gets out of the way.

That one decision — supervise a subprocess instead of call an API — is what made the rest of
this interesting.

The shape: a Flask control plane, one subprocess per project, SSE for the live feed

The core is a Flask app. Each project maps to a claude process. When you dispatch a task,
the server spawns (or reuses) that project's process, streams its stdout, and pushes the
tokens to the browser over Server-Sent Events. The dashboard is just N of those live feeds
tiled on a grid, each with a status: working, waiting on you, or idle.

Sounds simple. Here's where it got complicated -

Hurdle 1: the browser only gives you 6 SSE connections per origin

The dashboard wants a live stream per project. Open six projects and the seventh tile just…
hangs. That's not a bug in your code — it's the browser. Chromium caps concurrent
connections to a single origin at six, and a long-lived SSE stream holds one open the entire
time. Five streaming agents plus one stray reconnect and you've hit the ceiling.

The fix is to treat streams as scarce: open an SSE connection only for a project that's
actively running, and close it the moment the turn completes rather than holding it
open "just in case." Idle projects don't get a stream at all — they get polled cheaply. Once
streams are tied to activity instead of to tiles, the cap stops mattering.

Hurdle 2: Windows has an 8,191-character command line

To give an agent context, the natural instinct is to pass it as a CLI argument. On Windows,
cmd.exe truncates the whole command at 8,191 characters, silently. A decent system prompt
plus a project's memory blows past that instantly, and you get a corrupted invocation with
no error — the agent just starts wrong.

The fix: never pass context inline. Write it to a file and hand the CLI
--append-system-prompt-file. Same trick solves a second problem — the shell mangling quotes
and newlines in a big prompt. File in, path as the argument, done. Obvious in hindsight;
cost me an evening.

The persistent-process decision (and its sting)

You can spawn a fresh claude per turn, or keep one process alive across turns. Fresh-per-turn
is clean but slow and forgetful. A persistent process is fast and keeps its working state — so
that's the default. The sting: session-teardown logic that you assumed ran per-turn now only
runs when the process actually exits. Anything you hung off "end of turn" — writing memory,
releasing a stream, updating status — has to be re-anchored to the real lifecycle events, or
it silently stops firing. Persistent processes are worth it, but they move where "done"
happens, and every assumption downstream of "done" has to move with it.

Memory is the actual product; the process supervisor is just how you reach it

The plumbing above makes many agents runnable. It doesn't make them good. The real pain with
long-lived projects is that every session starts from zero — the agent re-reads the repo to work
out where you left off, re-derives decisions you already made, and burns turns rebuilding context
you both had yesterday. Solving that, not the tiling, is what makes running many agents
sustainable instead of just possible. It's the piece I've iterated on the most, and the piece
that took the longest to get right. Here's the pipeline.

Write — summarize from the real transcript, not the screen. At the end of a session (and,
because the process is persistent, at checkpoints during it — otherwise a hard kill loses the
thread), a cheap model condenses what happened into a few lines: decisions made, what's done,
what's next. The detail that matters is the source. The stdout you see scrolling in the
terminal has already dropped tool results and the model's own reasoning. The full-fidelity
record is the CLI's on-disk transcript (.jsonl), so the summarizer reads that and only falls
back to the stdout tail if it has to. Summarize from the screen and your memory is missing
exactly the parts that mattered.

Store — two regions, different owners. The per-project MEMORY.md is deliberately split. On
top, a small human-curated index — byte-preserved, the machinery never rewrites it. Below, a
sentinel-delimited managed region that only the write pipeline touches. Keeping the trustworthy
hand-written notes physically separate from the auto-captured churn is what lets you actually
rely on the file instead of babysitting it. Writes are per-project locked and atomic, so five
agents checkpointing at once never corrupt it.

Read — deterministic, not model-chosen. At the next dispatch a fixed slice of that memory is
injected as a read-floor before the agent does anything — it doesn't get to decide whether to
look. So it opens already knowing the state of the project instead of spelunking for it.

Trim — less is actually more. This is the counter-intuitive one. There's a hard byte budget
before the model effectively stops absorbing the injected context; push past it and recall gets
worse, not better. So the trim is ruthless and line-keyed — a lossless mechanical floor with a
model-driven condense above it — and everything it evicts goes to a permanent, searchable
archive that is never deleted. Cold storage, not a trash can: the working memory stays small
and sharp, and nothing is actually lost.

Two things I got wrong first, and they share a root cause. One: letting the agent freely rewrite
its own memory — it drifts, bloats, and within a week you can't trust a line of it. Two: assuming
more memory is better — see the trim. Both mistakes come from treating memory as a scratchpad the
agent controls, instead of a managed store the control plane keeps on the agent's behalf.
Once I flipped that ownership, the whole thing got trustworthy.

Standing agents: work that outlives the chat window

Once the control plane supervises processes and remembers state, a new thing becomes possible:
agents that keep working after you close the tab. A scheduler fires tasks on a cron; a
"charter" agent gets a standing goal and works it unattended in reversible steps, asking before
anything irreversible. That's the payoff of not being the message bus anymore — the work keeps
moving while you're away from the keyboard, and you check in from your phone to answer the one
thing that actually needs you.

The harder one: agents that are each other's blind spot

The question people ask next is "can two agents work on the same project at once?" The shallow
version is about file conflicts, and it has a boring answer: give each agent its own git worktree
and branch, and merge at the end.

But the interesting version isn't conflict — it's coordination. Two agents working on
interdependent features, not the same files. Agent B rebuilds something Agent A just finished.
Agent B picks an approach that A's just-landed change made obsolete. They never touch the same
line; they're just blind to each other.

And here's the trap: worktree isolation makes that worse. Isolation buys edit-safety by
putting each agent in its own checkout, so a sibling's landed commit is less visible, not more.
Safety and awareness pull in opposite directions, which means you can't design one without the
other.

I sat down to design this properly and the useful surprise was that the missing piece wasn't
infrastructure — it was delivery. The substrate was already there: a durable append-only
message bus, a shared knowledge store, SSE fan-out, a reconciler daemon pattern, and the same
read-floor injection the memory system uses. What didn't exist was the semantics on top: agents
publishing intentions ("about to touch the SSE slot manager") and completions ("landed
it, sha abc123, these paths"), and — the hard part — those events being pushed into a sibling's
context while it's mid-work, instead of sitting in a bus nobody polls.

So I built it. Two pieces, and they have to exist together.

Awareness. Agents publish intentions and completions to a per-project bus, and the server
does the publishing on their behalf wherever it can — deriving a completion from a commit's
touched paths rather than trusting an agent to remember a protocol. Siblings receive it as a
SIBLING ACTIVITY block in the read-floor, ranked by target-overlap: how much what you're
touching intersects what they're touching. The gate matters more than the mechanism — an
unfiltered firehose just teaches agents to ignore the section, exactly like a too-chatty
notification. Top-3, overlap-ranked, live agents only.

Isolation. Each concurrent agent gets its own git worktree and branch, with merge-back on
completion. The scoping detail I like most: only the 2nd+ concurrent agent is isolated. A
project with one agent — the overwhelmingly common case — pays nothing at all. Complexity that
only materializes when you actually need it.

Two rules ended up load-bearing, both learned the boring way: conflicts escalate and are never
auto-resolved
, and automatic cleanup must never destroy an agent's output. An orchestrator
that silently eats work it can't reconcile is worse than no orchestrator, because you stop being
able to trust any of it.

The thing I got wrong first was a race in the coordination state file. Two threads — the
awareness daemon and a dispatch — both did read-modify-write on the same JSON, and dedup records
silently vanished. Classic, and worth saying out loud because "agents coordinating" sounds like
an AI problem and the actual bug was a missing lock and a non-atomic write. Most of this work is
ordinary systems engineering wearing a novel hat.

What's still off: the third delivery rung. A sibling landing a change on paths you declared
intent to modify could interrupt your turn — the mechanism is built, but it ships default-off
until I've watched the quieter rungs behave. Interrupting a working agent is the kind of feature
that's very easy to regret, and I'd rather earn it with evidence than assume it.

What's still open: agents that actually finish

Here's the part I haven't solved yet, and it might matter most. Right now an agent hands the
turn back to you too eagerly — it hits the first fork or the first thing it's unsure about and
bounces the decision up. When you're supervising five of them, that's the message-bus problem
sneaking back in through the side door: you're no longer routing tasks, but you are the
completion-checker for every half-finished turn.

The direction I'm building toward is an agent that pushes to genuinely complete a task —
exhausts the reversible, obvious steps on its own and only comes back when it hits something that
truly needs a human: an irreversible action, a real ambiguity, a missing decision. Not reckless
autonomy — bounded autonomy, with the same "ask before anything irreversible" rule the standing
agents already follow. A finished turn you can review beats a half-finished turn you have to
unblock. It's not shipped yet; it's the next thing on the list, and I think it's the difference
between "many agents you babysit" and "many agents that actually carry work."

The honest limitations

It runs on your machine, so if the machine sleeps, the agent sleeps — and it tells you it did.
There's no always-on hosted tier, on purpose: I didn't want to sit in the middle of your API
traffic or your data. It's local-first, and that cuts both ways.

Where this lives

I bundled all of the above into an open-source tool called Clayrune (MIT). If you want to
see the shape without installing anything, there's a click-through demo:
👉 https://clayrune.io/demo — and the source (Flask, the subprocess supervisor, the memory
pipeline) is at https://github.com/ronle/clayrune.

I started this by saying that with many agents you become the message bus. The control plane
fixed the obvious half — I'm no longer the thing routing tasks and remembering state. The
coordination layer took a real bite out of the harder half: the agents aren't wholly blind to
each other anymore, so cross-agent context doesn't have to route through my head.

But I want to be careful not to overclaim, because the interesting part is what didn't get
solved. Agents now know what their siblings are doing. They're still not very good at deciding
what to do about it — "defer, adapt, or ask" is a judgment call, and right now that judgment is
a paragraph of context and a hope. The awareness plumbing turned out to be the tractable half;
the reasoning on top of it is wide open. And the related problem I mentioned earlier — agents
that push a task to genuine completion instead of bouncing every fork back at you — is still
ahead of me.

So the direction I'd point at isn't "run more agents." It's that the bus should terminate at
other agents, not always at a human — and then those agents should be good enough at reading
it that you don't have to referee. The first half is buildable today. The second half is the
actual frontier.

If you're running more than a couple of agents, I'd genuinely like to hear how you're handling
it — because I don't think one dashboard is the last word on this.

Top comments (0)