<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ran Levi</title>
    <description>The latest articles on DEV Community by Ran Levi (@ran_levi).</description>
    <link>https://dev.to/ran_levi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4052094%2F49a8c573-4d26-4b29-b9b3-fdf8af040287.png</url>
      <title>DEV Community: Ran Levi</title>
      <link>https://dev.to/ran_levi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ran_levi"/>
    <language>en</language>
    <item>
      <title>I run 5 Claude Code CLIs from one control plane. Here's the plumbing.</title>
      <dc:creator>Ran Levi</dc:creator>
      <pubDate>Sat, 01 Aug 2026 00:58:05 +0000</pubDate>
      <link>https://dev.to/ran_levi/i-run-5-claude-code-clis-from-one-control-plane-heres-the-plumbing-g9m</link>
      <guid>https://dev.to/ran_levi/i-run-5-claude-code-clis-from-one-control-plane-heres-the-plumbing-g9m</guid>
      <description>&lt;p&gt;Claude Code is great at being one agent in one terminal on one repo. The trouble starts at&lt;br&gt;
agent number three. I had five projects going, five terminal windows, and every morning I'd&lt;br&gt;
sit down and spend ten minutes just working out which agent was mid-task, which was blocked&lt;br&gt;
waiting on me, and which had quietly finished an hour ago.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Constraint that shaped everything: drive the CLI, not the API
&lt;/h3&gt;

&lt;p&gt;The obvious move is to hit Anthropic's API directly and build your own agent loop. I&lt;br&gt;
deliberately didn't. Two reasons.&lt;/p&gt;

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

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

&lt;p&gt;That one decision — supervise a subprocess instead of call an API — is what made the rest of&lt;br&gt;
this interesting.&lt;/p&gt;

&lt;h3&gt;
  
  
  The shape: a Flask control plane, one subprocess per project, SSE for the live feed
&lt;/h3&gt;

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

&lt;p&gt;Sounds simple. Here's where it got complicated - &lt;/p&gt;

&lt;h3&gt;
  
  
  Hurdle 1: the browser only gives you 6 SSE connections per origin
&lt;/h3&gt;

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

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

&lt;h3&gt;
  
  
  Hurdle 2: Windows has an 8,191-character command line
&lt;/h3&gt;

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

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

&lt;h3&gt;
  
  
  The persistent-process decision (and its sting)
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  Memory is the actual product; the process supervisor is just how you reach it
&lt;/h3&gt;

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

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

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

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

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

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

&lt;h3&gt;
  
  
  Standing agents: work that outlives the chat window
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  The harder one: agents that are each other's blind spot
&lt;/h3&gt;

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

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

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

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

&lt;p&gt;So I built it. Two pieces, and they have to exist together.&lt;/p&gt;

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

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

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

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

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

&lt;h3&gt;
  
  
  What's still open: agents that actually finish
&lt;/h3&gt;

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

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

&lt;h3&gt;
  
  
  The honest limitations
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  Where this lives
&lt;/h3&gt;

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

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

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

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

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

</description>
      <category>claude</category>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
