Selected work / ClaudeMail / 2026-11-13
Mail between Claude sessions on eight computers
What it took to let Claude Code sessions on different machines hand each other work, and what they send each other now.
- What
- A mail system for Claude Code sessions. A session on one computer picks a session on another (or asks for a new one) and sends it a message. The message arrives as a visible turn in the Claude Code desktop app, and the reply comes back the same way.
- My part
- The requirements, the design decisions and the reviews. Claude Code sessions wrote, tested and repaired the code at my direction.
- Status
- Runs every day since 2026-09-24, and on all eight machines (four Windows PCs, four Macs) since 2026-11-09. Public on GitHub under the MIT license since 2026-11-11.
- Methods
- Python (standard library only), Resilio Sync as the transport, launchd and Windows scheduled tasks, failure audits by message count, independent review agents, tests against a control copy of the old code.
By September 2026 I was the mail. My house had five computers then (eight now), and most of them ran Claude Code sessions with their own jobs: one transcribed audio, one built iOS apps, one held the archives. Whenever one of them needed something from another, I copied the prompt from one screen, pasted it on the other, waited, and carried the answer back. Claude Code's own SendMessage can wake a session on another machine, but session names differ from machine to machine and nothing confirms delivery, so between machines the reliable channel was me.
Early on 2026-09-24 I asked for "a pseudo email system, so that each Claude can reach the other Claude instances on other computers". The transport was already there, because Resilio Sync keeps one folder tree identical on all machines. Six minutes later a session had written mail.py, the installers and the docs, and installed a file watcher. A message was a file dropped into another machine's inbox folder, the watcher noticed it, and a hook handed it to whichever session I typed into next.
It worked. Mostly.
Mail in the wrong place
The first symptom was small. Mail sent to a machine went into whatever session happened to be running there, which was often the wrong one. Over the next three days the fixes piled up (a folder per session, a hook that woke idle sessions after each turn, a headless "mailroom" run for mail that nobody claimed). Each fix made sense on its own. Together they made a system I could no longer predict.
On 2026-09-26 I recorded a voice note that ClaudeMail was not really working reliably yet. A review session went through everything and found 34 distinct failures, 18 of them still open. Most of them came from three choices: mail was addressed to machines or sessions when the sender actually meant a topic, every session raced to claim mail from inside its hooks, and the headless mailroom could only write notes, so requests sat waiting for me (once for almost eight hours).
The redesign addressed mail to a desk, which is a project on a machine. I asked to drop the headless runs entirely, because their work was invisible to me and doubled my maintenance. In their place every machine got a pinned "Post office" session whose only job was to wake the right session (and later a second pinned session, the Steward, for mail addressed to the machine as a whole). The rewrite went live the next day. That afternoon I said in another voice note that the second version was working, and that evening I called it a weekend project. It took seven weeks.
Counting instead of trusting
Then I had the desks counted. On 2026-09-30 an audit of all 371 messages since the rewrite found 16% delivered to the wrong session, and 27% of replies. A ten-day review on 2026-10-12 split the picture in two. Mail for a session that was already listening arrived fast (median 12 s), and mail addressed to one specific session reached it 232 times out of 233. But the wake-up path had never worked on its own: the Post office had been asked to wake a session 26 times and woke nobody.
The cause was in how the desktop app runs sessions. The app updates itself when it is idle, and that update stops every session process, including the listeners inside them, and nothing restarted those. A week later the Post office went deaf again for a different reason: its renewal turn hit the weekly usage limit, and a turn that ends in an API error runs a different hook (StopFailure, not Stop), so its listener never came back. A long-lived session is not a daemon, and the whole design treated it as one.
On 2026-10-20 both alternative ways to wake a session failed their tests. One was refused the cross-session tool because it needed a person's approval, and the other answered "Login expired". I wrote "Overall I'd like to move away from me being involved in mail delivery", because four weeks in, stuck mail still ended up with me.
Let the sender choose
The way out came the same day. The receiving machine had been guessing all along which session a message was meant for. The sender doesn't have to guess, because the sender knows what the message is about. So I asked for a two-stage process: the sender first asks what sessions exist on the target computer, then picks one, or asks for a new one.
Version 4 went live on 2026-10-26 and removed the Post office, the Steward and the waiting hooks. The code shrank by about half. The sending session reads a list of the target's sessions (each machine publishes one) and names the session it wants. The receiving machine writes straight into that session's inbox socket if it runs, reopens it with the app's claude://resume link if it is closed, and creates it if it is new. Creating a session uses a local command of the command-line Claude that needs no model call and no sign-in, so it doesn't break every time a login expires. Live tests with real sessions the same evening: a running session answered in 7 s, a new session had its mail 4 s after it arrived, and a closed one answered 9 s after it was reopened.
Opened sessions then stopped at their first permission prompt, with nobody there to answer it, so auto mode became the default permission on every machine. Auto mode still refuses some steps (unpinning a session, editing settings), and I think it is right to. Those steps come to me, which is fine: approving one step is a very different job from carrying mail.
Root causes, not hotfixes
Four days later a review of version 4 found 355 of 358 messages delivered, none to the wrong session, median 12 s. Routing was solved. What remained was availability and feedback: one machine's sync had silently split, one bad record could crash-loop a dispatcher for hours, some opened sessions never started, and when work was blocked the sender never heard about it. I asked to solve each of these at the root rather than with another hotfix.
Five investigations ran, with a design critic and a fact checker on top. The most useful finding came from reading the desktop app's bundled code: the app caps its session processes at max(6, floor(memory / 3 GiB)) and silently declines background starts above that cap. That explained the sessions that were opened but never started. Three deploys followed. The first made failures visible, so a crashing dispatcher reports its crash and every machine warns about peers that are crash-looping, silent or ahead in time. The second delivers each message in its own process, so a bad message fails alone, and it tells the sender why a session didn't start, quoting the app's own log line. The third gave messages globally unique ids (they had been unique per machine, which only showed up when one machine sent the same checklist to all the others).
The method mattered more than any single fix. Every change ran against a control copy of the old code, and each new check had to fail there first (twice a new check also passed on the old code, and only the control run showed it). Independent review agents read the first candidates of the three deploys and found 8, 10 and 2 serious defects, all fixed before the deploy. A canary on the live mailbox found a failure that no sandbox had.
What it carries now
Since 2026-09-24 ClaudeMail has filed about 2,700 messages (about 2,500 separate sends, since some went to several machines). Most of them are not conversations. Of the 1,081 messages filed in the first 13 days of November, 306 are notices from the job scheduler that shares CPU and memory between the machines (lease granted, job started, job finished), 193 are scheduled jobs, 406 are replies, and the remaining 176 are new requests and notices from one session to another. Typical requests: run the iPhone unit tests on the Mac mini, take new website screenshots of an app, move a service from one machine to another in two phases with a handshake, coordinate a database reset across the house.
The synced folder is now the weakest part. Resilio is fine for documents and code, but as a message bus one delivery in ten takes longer than 97 s, the slowest take minutes, and a one-way split between two machines can go unnoticed for weeks. Rotating report files had also left about 80,000 deleted-file records behind, which stalled the newest machine's sync for twelve days. A small peer-to-peer postman over Tailscale is built, and ClaudeMail has a switch to move its machine reports onto it (for now the switch still points at the synced folder).
ClaudeMail is personal infrastructure, published as it runs, and probably not for everyone. It leans on parts of the desktop app that are not a public API (the session registry, the inbox socket, the resume link, the session store), so any app update can break it. The repository on GitHub is meant as a worked example and a starting point.
What I'd tell a colleague
Three things:
- A long-lived session is not a daemon. Anything that hangs on one goes deaf without notice, so whatever listens has to be a real service that restarts itself.
- Let the side that knows the intent decide. Most of my routing bugs came from the receiver guessing what the sender meant.
- Count. Each redesign of ClaudeMail followed a review that counted failures or messages.
On the evening of 2026-10-31 a session on my laptop was fixing a crash in a TestFlight build of Inklight at 90% playback speed. It had the fix and a new test, but unit tests don't run on that laptop. At 19:07 it sent the test run to the Mac mini and asked for a new session there, titled "Inklight floor-sliver tests". The new session waited until the simulator was free, ran 543 tests under a lease (0 failures), shut the simulator down, deleted its scratch files and replied at 19:22. Neither message went through me.