<?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: Zika Zag</title>
    <description>The latest articles on DEV Community by Zika Zag (@zika_zag_c43dc387e13f45ea).</description>
    <link>https://dev.to/zika_zag_c43dc387e13f45ea</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%2F2356000%2F43b1bff8-44bc-4014-9081-2907b3d8077d.jpg</url>
      <title>DEV Community: Zika Zag</title>
      <link>https://dev.to/zika_zag_c43dc387e13f45ea</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zika_zag_c43dc387e13f45ea"/>
    <language>en</language>
    <item>
      <title>Keeping a fleet of Claude Code sessions alive when everything wants to kill them</title>
      <dc:creator>Zika Zag</dc:creator>
      <pubDate>Sat, 15 Aug 2026 18:04:58 +0000</pubDate>
      <link>https://dev.to/zika_zag_c43dc387e13f45ea/keeping-a-fleet-of-claude-code-sessions-alive-when-everything-wants-to-kill-them-4gl1</link>
      <guid>https://dev.to/zika_zag_c43dc387e13f45ea/keeping-a-fleet-of-claude-code-sessions-alive-when-everything-wants-to-kill-them-4gl1</guid>
      <description>&lt;p&gt;I run Claude Code a lot: often several sessions at once, often on a remote box over SSH, often for hours. The CLI is great at this. What I was &lt;em&gt;not&lt;/em&gt; great at was not losing the sessions.&lt;/p&gt;

&lt;p&gt;A terminal tab closes. The laptop sleeps. The SSH tunnel drops for four seconds on hotel wifi. Any one of these and a running &lt;code&gt;claude -p&lt;/code&gt; session is just... gone, mid-thought, with whatever it was doing. I got tired of it and built a web UI (Walnut) whose entire job for sessions is: &lt;strong&gt;the session outlives the things that try to kill it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is about the part that was genuinely hard (the resilience model), not the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive version, and why it dies
&lt;/h2&gt;

&lt;p&gt;The obvious design is: browser → my server → &lt;code&gt;spawn('claude', ...)&lt;/code&gt;. The server owns the child process. This works until the server restarts (deploy, crash, OOM), and now every session it was parenting dies with it. For a tool whose pitch is "your sessions don't vanish," that's the one thing it can't do.&lt;/p&gt;

&lt;p&gt;The second problem is subtler. &lt;code&gt;claude -p --input-format stream-json&lt;/code&gt; is &lt;strong&gt;long-running&lt;/strong&gt;, not per-turn. One CLI process stays alive across many messages, reading new input between turns. So you can't treat a turn ending as the session ending: the process is supposed to sit there idle, waiting. If your lifecycle logic assumes "turn done = process done," you'll keep reaping live sessions, and you'll never understand why they keep disappearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually worked: a daemon that nothing owns
&lt;/h2&gt;

&lt;p&gt;The model I landed on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;browser  ──ws──▶  walnut server  ──ssh tunnel──▶  daemon (remote)  ──spawn──▶  claude -p
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key move is that &lt;strong&gt;the daemon is not a child of my server.&lt;/strong&gt; It's a small long-lived process on the remote host. My server connects to it; it doesn't depend on my server to live. When my server restarts, the daemon (and every &lt;code&gt;claude&lt;/code&gt; process under it) keeps running. The server reconnects and &lt;em&gt;re-adopts&lt;/em&gt; the sessions that were already there.&lt;/p&gt;

&lt;p&gt;That adoption step is the whole ballgame, and it's where the bugs live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things that took real debugging
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The FIFO has to be held open, or the pipe collapses between turns.&lt;/strong&gt;&lt;br&gt;
Each session's stdin is a named pipe. If the only writer closes its handle after a turn, the reader sees EOF and the CLI exits. The fix is unglamorous: the daemon holds the FIFO open with &lt;code&gt;O_RDWR&lt;/code&gt; for the life of the session, so the pipe survives the gaps between turns. The process is only reaped by a real death or a long idle timer, never by "the turn ended."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "Re-adopt" must skip what it already adopted.&lt;/strong&gt;&lt;br&gt;
On restart the daemon reconciles its registry, then scans the host for orphaned session process-groups it should pick back up. The non-obvious bug: if the scan doesn't skip sessions it &lt;em&gt;already&lt;/em&gt; re-adopted from the registry, you double-adopt and get duplicate streams. One line (&lt;code&gt;if (already_have(sid)) continue&lt;/code&gt;), but it cost an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A clean turn-end and a crash look the same at the exit code, until you read the tail.&lt;/strong&gt;&lt;br&gt;
When a process does die, you want to tell the user &lt;em&gt;why&lt;/em&gt;. A turn that completed cleanly and a process that crashed can surface the same way. The session's output stream (JSONL) is the source of truth: if the tail shows a clean result line, normalize the exit to "completed"; otherwise it's a real failure. Without this, every normal turn-end shows up in the UI as "exited -1," which is alarming and wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transfer problem (a fun one)
&lt;/h2&gt;

&lt;p&gt;Deploying a new daemon binary to the remote host should be easy. It wasn't, because the corporate SSH proxy in my setup silently kills any transfer over ~5MB. &lt;code&gt;scp&lt;/code&gt; of the binary just hangs forever.&lt;/p&gt;

&lt;p&gt;The fix is dumb and I love it: gzip the binary, &lt;strong&gt;chunk it into 1MB pieces, send each over its own SSH connection,&lt;/strong&gt; reassemble on the other side, retry per-chunk. Each chunk is small enough to slip under the proxy's limit. It auto-deploys on the next session send if the local and remote versions differ, and falls back to shipping the (tiny) source if the chunked binary fails. No more manual copies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone building similar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A long-running agent process is a &lt;em&gt;resource you reconnect to&lt;/em&gt;, not a function you call. Design the reconnect path first; it's the hard part.&lt;/li&gt;
&lt;li&gt;Make the durable state live where the work lives (on the remote, in a file), not in the process that happens to be orchestrating it right now.&lt;/li&gt;
&lt;li&gt;When two different events (clean exit / crash; spawn / re-adopt) can look identical, find the one piece of ground-truth that distinguishes them and key everything off that. Don't infer it from timing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Walnut is open source (MIT) if you want to read the actual code: &lt;a href="https://github.com/EvanZhang008/open-walnut" rel="noopener noreferrer"&gt;https://github.com/EvanZhang008/open-walnut&lt;/a&gt;. The session layer (&lt;code&gt;src/providers/&lt;/code&gt;) is the most interesting part. There is also a 2-minute demo of the whole thing (calendar + iPhone app included): &lt;a href="https://youtu.be/LbdQJXPwGVE" rel="noopener noreferrer"&gt;https://youtu.be/LbdQJXPwGVE&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>cli</category>
      <category>software</category>
      <category>tools</category>
    </item>
  </channel>
</rss>
