{"slug": "i-run-5-claude-code-clis-from-one-control-plane-here-s-the-plumbing", "title": "I run 5 Claude Code CLIs from one control plane. Here's the plumbing.", "summary": "A developer built a control plane that supervises multiple Claude Code CLI processes from a single dashboard, enabling five agents to run concurrently without becoming the message bus. The system uses a Flask app to spawn and monitor claude subprocesses, streams output via Server-Sent Events, and handles browser connection limits and Windows command-line truncation by writing context to files. The developer chose to drive the CLI rather than the API to keep costs on the existing subscription and preserve the agent's built-in capabilities.", "body_md": "Claude Code is great at being one agent in one terminal on one repo. The trouble starts at\n\nagent number three. I had five projects going, five terminal windows, and every morning I'd\n\nsit down and spend ten minutes just working out which agent was mid-task, which was blocked\n\nwaiting on me, and which had quietly finished an hour ago.\n\nThe thing nobody tells you about running multiple agents is that **you become the message\nbus.** Every agent routes through your attention. Scale that to five and the agents aren't\n\nThe obvious move is to hit Anthropic's API directly and build your own agent loop. I\n\ndeliberately didn't. Two reasons.\n\nFirst, cost and trust: driving the `claude`\n\nCLI means the work runs on your existing Claude\n\nsubscription, not a metered API key. It costs exactly what Claude Code costs you today, and\n\nnothing routes through a server I run. For a tool people install on their own machine, \"your\n\nsubscription, your keys, your data\" isn't a feature — it's the whole deal.\n\nSecond, the CLI *already is* the agent. It has the tool-use loop, the permission model, the\n\nfile editing, the MCP support. Re-implementing that against the raw API is throwing away the\n\nbest part. So the design flipped: instead of *being* the agent, the control plane **spawns\nand supervises claude CLI processes** and gets out of the way.\n\nThat one decision — supervise a subprocess instead of call an API — is what made the rest of\n\nthis interesting.\n\nThe core is a Flask app. Each project maps to a `claude`\n\nprocess. When you dispatch a task,\n\nthe server spawns (or reuses) that project's process, streams its stdout, and pushes the\n\ntokens to the browser over Server-Sent Events. The dashboard is just N of those live feeds\n\ntiled on a grid, each with a status: **working, waiting on you, or idle.**\n\nSounds simple. Here's where it got complicated -\n\nThe dashboard wants a live stream per project. Open six projects and the seventh tile just…\n\nhangs. That's not a bug in your code — it's the browser. Chromium caps concurrent\n\nconnections to a single origin at six, and a long-lived SSE stream holds one open the entire\n\ntime. Five streaming agents plus one stray reconnect and you've hit the ceiling.\n\nThe fix is to treat streams as scarce: open an SSE connection only for a project that's\n\nactively `running`\n\n, and **close it the moment the turn completes** rather than holding it\n\nopen \"just in case.\" Idle projects don't get a stream at all — they get polled cheaply. Once\n\nstreams are tied to activity instead of to tiles, the cap stops mattering.\n\nTo give an agent context, the natural instinct is to pass it as a CLI argument. On Windows,\n\n`cmd.exe`\n\ntruncates the whole command at 8,191 characters, silently. A decent system prompt\n\nplus a project's memory blows past that instantly, and you get a corrupted invocation with\n\nno error — the agent just starts wrong.\n\nThe fix: never pass context inline. Write it to a file and hand the CLI\n\n`--append-system-prompt-file`\n\n. Same trick solves a second problem — the shell mangling quotes\n\nand newlines in a big prompt. File in, path as the argument, done. Obvious in hindsight;\n\ncost me an evening.\n\nYou can spawn a fresh `claude`\n\nper turn, or keep one process alive across turns. Fresh-per-turn\n\nis clean but slow and forgetful. A persistent process is fast and keeps its working state — so\n\nthat's the default. The sting: session-teardown logic that you assumed ran per-turn now only\n\nruns when the process actually exits. Anything you hung off \"end of turn\" — writing memory,\n\nreleasing a stream, updating status — has to be re-anchored to the real lifecycle events, or\n\nit silently stops firing. Persistent processes are worth it, but they move where \"done\"\n\nhappens, and every assumption downstream of \"done\" has to move with it.\n\nThe plumbing above makes many agents *runnable*. It doesn't make them *good*. The real pain with\n\nlong-lived projects is that every session starts from zero — the agent re-reads the repo to work\n\nout where you left off, re-derives decisions you already made, and burns turns rebuilding context\n\nyou both had yesterday. Solving *that*, not the tiling, is what makes running many agents\n\nsustainable instead of just possible. It's the piece I've iterated on the most, and the piece\n\nthat took the longest to get right. Here's the pipeline.\n\n**Write — summarize from the real transcript, not the screen.** At the end of a session (and,\n\nbecause the process is persistent, at checkpoints *during* it — otherwise a hard kill loses the\n\nthread), a cheap model condenses what happened into a few lines: decisions made, what's done,\n\nwhat's next. The detail that matters is the *source*. The stdout you see scrolling in the\n\nterminal has already dropped tool results and the model's own reasoning. The full-fidelity\n\nrecord is the CLI's on-disk transcript (`.jsonl`\n\n), so the summarizer reads *that* and only falls\n\nback to the stdout tail if it has to. Summarize from the screen and your memory is missing\n\nexactly the parts that mattered.\n\n**Store — two regions, different owners.** The per-project `MEMORY.md`\n\nis deliberately split. On\n\ntop, a small human-curated index — byte-preserved, the machinery never rewrites it. Below, a\n\nsentinel-delimited *managed* region that only the write pipeline touches. Keeping the trustworthy\n\nhand-written notes physically separate from the auto-captured churn is what lets you actually\n\nrely on the file instead of babysitting it. Writes are per-project locked and atomic, so five\n\nagents checkpointing at once never corrupt it.\n\n**Read — deterministic, not model-chosen.** At the next dispatch a fixed slice of that memory is\n\ninjected as a read-*floor* before the agent does anything — it doesn't get to decide whether to\n\nlook. So it opens already knowing the state of the project instead of spelunking for it.\n\n**Trim — less is actually more.** This is the counter-intuitive one. There's a hard byte budget\n\nbefore the model effectively stops absorbing the injected context; push past it and recall gets\n\n*worse*, not better. So the trim is ruthless and line-keyed — a lossless mechanical floor with a\n\nmodel-driven condense above it — and everything it evicts goes to a permanent, searchable\n\narchive that is **never** deleted. Cold storage, not a trash can: the working memory stays small\n\nand sharp, and nothing is actually lost.\n\nTwo things I got wrong first, and they share a root cause. One: letting the agent freely rewrite\n\nits own memory — it drifts, bloats, and within a week you can't trust a line of it. Two: assuming\n\nmore memory is better — see the trim. Both mistakes come from treating memory as a scratchpad the\n\n*agent* controls, instead of a managed store the *control plane* keeps on the agent's behalf.\n\nOnce I flipped that ownership, the whole thing got trustworthy.\n\nOnce the control plane supervises processes and remembers state, a new thing becomes possible:\n\nagents that keep working after you close the tab. A scheduler fires tasks on a cron; a\n\n\"charter\" agent gets a standing goal and works it unattended in reversible steps, asking before\n\nanything irreversible. That's the payoff of not being the message bus anymore — the work keeps\n\nmoving while you're away from the keyboard, and you check in from your phone to answer the one\n\nthing that actually needs you.\n\nThe question people ask next is \"can two agents work on the same project at once?\" The shallow\n\nversion is about file conflicts, and it has a boring answer: give each agent its own git worktree\n\nand branch, and merge at the end.\n\nBut the interesting version isn't conflict — it's **coordination**. Two agents working on\n\n*interdependent* features, not the same files. Agent B rebuilds something Agent A just finished.\n\nAgent B picks an approach that A's just-landed change made obsolete. They never touch the same\n\nline; they're just blind to each other.\n\nAnd here's the trap: **worktree isolation makes that worse.** Isolation buys edit-safety by\n\nputting each agent in its own checkout, so a sibling's landed commit is *less* visible, not more.\n\nSafety and awareness pull in opposite directions, which means you can't design one without the\n\nother.\n\nI sat down to design this properly and the useful surprise was that the missing piece wasn't\n\ninfrastructure — it was *delivery*. The substrate was already there: a durable append-only\n\nmessage bus, a shared knowledge store, SSE fan-out, a reconciler daemon pattern, and the same\n\nread-floor injection the memory system uses. What didn't exist was the semantics on top: agents\n\npublishing **intentions** (\"about to touch the SSE slot manager\") and **completions** (\"landed\n\nit, sha abc123, these paths\"), and — the hard part — those events being *pushed* into a sibling's\n\ncontext while it's mid-work, instead of sitting in a bus nobody polls.\n\nSo I built it. Two pieces, and they have to exist together.\n\n**Awareness.** Agents publish intentions and completions to a per-project bus, and the server\n\ndoes the publishing *on their behalf* wherever it can — deriving a completion from a commit's\n\ntouched paths rather than trusting an agent to remember a protocol. Siblings receive it as a\n\n`SIBLING ACTIVITY`\n\nblock in the read-floor, ranked by **target-overlap**: how much what you're\n\ntouching intersects what they're touching. The gate matters more than the mechanism — an\n\nunfiltered firehose just teaches agents to ignore the section, exactly like a too-chatty\n\nnotification. Top-3, overlap-ranked, live agents only.\n\n**Isolation.** Each concurrent agent gets its own git worktree and branch, with merge-back on\n\ncompletion. The scoping detail I like most: **only the 2nd+ concurrent agent is isolated.** A\n\nproject with one agent — the overwhelmingly common case — pays nothing at all. Complexity that\n\nonly materializes when you actually need it.\n\nTwo rules ended up load-bearing, both learned the boring way: **conflicts escalate and are never\nauto-resolved**, and\n\nThe thing I got wrong first was a race in the coordination state file. Two threads — the\n\nawareness daemon and a dispatch — both did read-modify-write on the same JSON, and dedup records\n\nsilently vanished. Classic, and worth saying out loud because \"agents coordinating\" sounds like\n\nan AI problem and the actual bug was a missing lock and a non-atomic write. Most of this work is\n\nordinary systems engineering wearing a novel hat.\n\nWhat's still off: the third delivery rung. A sibling landing a change on paths you declared\n\nintent to modify *could* interrupt your turn — the mechanism is built, but it ships **default-off**\n\nuntil I've watched the quieter rungs behave. Interrupting a working agent is the kind of feature\n\nthat's very easy to regret, and I'd rather earn it with evidence than assume it.\n\nHere's the part I *haven't* solved yet, and it might matter most. Right now an agent hands the\n\nturn back to you too eagerly — it hits the first fork or the first thing it's unsure about and\n\nbounces the decision up. When you're supervising five of them, that's the message-bus problem\n\nsneaking back in through the side door: you're no longer routing tasks, but you *are* the\n\ncompletion-checker for every half-finished turn.\n\nThe direction I'm building toward is an agent that pushes to genuinely *complete* a task —\n\nexhausts the reversible, obvious steps on its own and only comes back when it hits something that\n\ntruly needs a human: an irreversible action, a real ambiguity, a missing decision. Not reckless\n\nautonomy — *bounded* autonomy, with the same \"ask before anything irreversible\" rule the standing\n\nagents already follow. A finished turn you can review beats a half-finished turn you have to\n\nunblock. It's not shipped yet; it's the next thing on the list, and I think it's the difference\n\nbetween \"many agents you babysit\" and \"many agents that actually carry work.\"\n\nIt runs on your machine, so if the machine sleeps, the agent sleeps — and it tells you it did.\n\nThere's no always-on hosted tier, on purpose: I didn't want to sit in the middle of your API\n\ntraffic or your data. It's local-first, and that cuts both ways.\n\nI bundled all of the above into an open-source tool called **Clayrune** (MIT). If you want to\n\nsee the shape without installing anything, there's a click-through demo:\n\n👉 [https://clayrune.io/demo](https://clayrune.io/demo) — and the source (Flask, the subprocess supervisor, the memory\n\npipeline) is at [https://github.com/ronle/clayrune](https://github.com/ronle/clayrune).\n\nI started this by saying that with many agents you become the message bus. The control plane\n\nfixed the obvious half — I'm no longer the thing routing tasks and remembering state. The\n\ncoordination layer took a real bite out of the harder half: the agents aren't wholly blind to\n\neach other anymore, so cross-agent context doesn't have to route through my head.\n\nBut I want to be careful not to overclaim, because the interesting part is what *didn't* get\n\nsolved. Agents now know what their siblings are doing. They're still not very good at deciding\n\nwhat to *do* about it — \"defer, adapt, or ask\" is a judgment call, and right now that judgment is\n\na paragraph of context and a hope. The awareness plumbing turned out to be the tractable half;\n\nthe reasoning on top of it is wide open. And the related problem I mentioned earlier — agents\n\nthat push a task to genuine completion instead of bouncing every fork back at you — is still\n\nahead of me.\n\nSo the direction I'd point at isn't \"run more agents.\" It's that the bus should terminate at\n\nother **agents**, not always at a human — and then those agents should be good enough at reading\n\nit that you don't have to referee. The first half is buildable today. The second half is the\n\nactual frontier.\n\nIf you're running more than a couple of agents, I'd genuinely like to hear how you're handling\n\nit — because I don't think one dashboard is the last word on this.", "url": "https://wpnews.pro/news/i-run-5-claude-code-clis-from-one-control-plane-here-s-the-plumbing", "canonical_source": "https://dev.to/ran_levi/i-run-5-claude-code-clis-from-one-control-plane-heres-the-plumbing-g9m", "published_at": "2026-08-01 00:58:05+00:00", "updated_at": "2026-08-01 01:10:27.235275+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Claude Code", "Anthropic", "Flask"], "alternates": {"html": "https://wpnews.pro/news/i-run-5-claude-code-clis-from-one-control-plane-here-s-the-plumbing", "markdown": "https://wpnews.pro/news/i-run-5-claude-code-clis-from-one-control-plane-here-s-the-plumbing.md", "text": "https://wpnews.pro/news/i-run-5-claude-code-clis-from-one-control-plane-here-s-the-plumbing.txt", "jsonld": "https://wpnews.pro/news/i-run-5-claude-code-clis-from-one-control-plane-here-s-the-plumbing.jsonld"}}