DEV Community

Beni
Beni

Posted on

We Accidentally Reinvented SMTP for Claude Code Instances

We didn't plan to build an email system. We planned to copy a file.

Day Zero: The Clipboard

Three servers. Up to six concurrent Claude Code instances across them. Same projects, different contexts. One server handles the autonomous dev agent. Another handles product work and competitive intel. A third joined for extra compute.

The problem is obvious: they can't talk to each other.

Day one solution: a shared directory. Write a markdown file, SCP it to the other server. The other instance reads it next session.

scp review-notes.md root@10.0.0.2:~/mailbox/
Enter fullscreen mode Exit fullscreen mode

That's it. That was the entire communication infrastructure. And honestly? It worked. For about two days.

The Five Failures That Built the System

Failure 1: "Did they see this?"

You drop a brief. Next session, you have no idea if the other instance read it. Did the human show it to them? Did they skip it? Is the action item in progress or sitting untouched?

So we added a SQLite inbox. Every file gets registered with a timestamp and a read_at column. NULL means unread. Not NULL means processed.

Failure 2: "What changed since last time?"

Session context resets every conversation. You can't remember what you saw last time. ls -lat tells you what's newest, not what's unread.

So we added a digest command. Query WHERE read_at IS NULL, read each file, extract action items, mark everything read. One command, full inbox processed.

ClaudeMail Digest — 5 unread

1. [server2 -> all] deploy-css-migration-merged.md
   PR #21 merged, needs production deploy

2. [server2 -> all] cost-model-fitted.md
   281 tasks trained, p75 $0.47, needs wiring into dispatcher
Enter fullscreen mode Exit fullscreen mode

Failure 3: "Who's online right now?"

Multiple servers, multiple instances per server. When you send a brief, you don't know if anyone's listening. Are you talking to an empty room?

So we built a roster. Each instance heartbeats every 60 seconds, updating its last_seen timestamp. Stale instances get marked offline automatically. PID-locked callsigns prevent identity collisions when multiple instances run on the same server — run as many as you need.

Roster
────────────────────────────────────
  alpha     server1     online    30s ago
  bravo     server1     offline   3h ago
  charlie   server2     online    1m ago
  delta     server3     online    45s ago
────────────────────────────────────
Enter fullscreen mode Exit fullscreen mode

Failure 4: "That action item was done three days ago"

Briefs generate action items. Action items pile up. Nobody sweeps them. Three sessions later, half the list is stale — completed, merged, or superseded by a newer brief.

So we added an action tracker. pending, wip, done, skip. Grouped by project. Prioritized by urgency. A checkup command surfaces stale work-in-progress and overdue items.

Failure 5: "The poller died and nobody noticed"

The first background poller ran in a bash loop with sleep 20. Bash loops die silently. SSH timeouts, process signals, shell exits — any of them kill the loop. You think you're monitoring for new mail. You're not.

Three iterations later: a Node.js MCP server with a built-in poller. Runs inside Claude Code's process tree, dies when the session dies, starts when the session starts. A status line at the bottom of the terminal shows unread count at all times:

✉ ClaudeMail v1.0.0 | 3 unread | 5 actions | alpha
Enter fullscreen mode Exit fullscreen mode

And a startup hook checks your inbox on every first message:

[mail] 3 unread briefs. Run /mail digest
Enter fullscreen mode Exit fullscreen mode

No manual polling. No commands to remember. Mail just shows up.

The Architecture

After all the iterations, the stack is almost comically simple:

  • Transport: HTTP mesh over Tailscale (or any private network)
  • Format: Markdown files with YAML-ish frontmatter (From, To, Date, Action)
  • Storage: SQLite per node (inbox, actions, read receipts, roster)
  • Identity: PID-locked callsigns, any number per server
  • Interface: 12 MCP tools registered directly with Claude Code
  • Notifications: status line + startup hook (MCP stderr not yet rendered by Claude Code)

No message broker. No pub/sub. No WebSockets. No API keys. No accounts. No cloud service.

Each server runs a lightweight HTTP gateway. Briefs flow directly between nodes — no central server, no relay. The poller checks every 20 seconds: hash the mailbox, compare to last known state, pull new files from remote nodes.

┌─────────────────┐                     ┌─────────────┐
│    Server A      │       HTTP          │  Server B   │
│    :3300         │◄───────────────────►│  :3301      │
│                  │   briefs + pings    │             │
│ ┌──────┐┌──────┐│                     │ ┌─────────┐ │
│ │alpha ││bravo ││                     │ │ charlie │ │
│ └──────┘└──────┘│                     │ └─────────┘ │
│ ┌──────┐        │                     └─────────────┘
│ │delta │        │       HTTP          ┌─────────────┐
│ └──────┘        │◄───────────────────►│  Server C   │
└─────────────────┘                     │  :3302      │
                                        │ ┌─────────┐ │
                                        │ │  echo   │ │
                                        │ └─────────┘ │
                                        └─────────────┘
Enter fullscreen mode Exit fullscreen mode

The most reliable part of our infrastructure is the part with the least infrastructure.

What Twenty-Two Briefs in One Night Looks Like

One session. Two instances designing a B2B product. Eighty kilobytes of specs bouncing between servers:

  • Pricing models (5 tiers, 2 billing modes, annual discounts)
  • API routes (20+), database tables (6), notification types (16)
  • Competitive positioning against two new market entrants
  • Code review results, PR approvals, deploy confirmations
  • Architecture decisions with rationale and trade-offs

Twenty-two briefs. Zero lost. Zero duplicated. Zero corrupted. The "real" infrastructure — GitHub API, config files, database migrations — failed four times in the same session. The file drops never failed once.

The Uncomfortable Truth

We reinvented email. Not web email. Not Gmail. The original thing. Store-and-forward messaging between two nodes, with local mailboxes, read receipts, and a directory protocol.

SMTP was designed in 1982 for exactly this problem: two computers that need to exchange messages asynchronously. We arrived at the same architecture forty-four years later, with markdown instead of RFC 822 headers and SQLite instead of mbox files.

This isn't a failure of imagination. It's convergent evolution. When two agents need to communicate asynchronously, track what they've said, know if the other side received it, and extract actionable items from the conversation — you get email. Every time. The medium doesn't matter. The protocol emerges from the problem.

It's Open Source Now

We shipped it. MIT license. One clean commit, zero history leaks.

github.com/ai461/claudemail

12 MCP tools. 125 tests. ~2,100 lines of TypeScript. Clone it, point it at your servers, and your Claude Code instances can talk to each other.

It ships with a /mail skill for Claude Code, a startup hook that checks your inbox on launch, and a status line config that shows unread count at all times. Zero friction — mail just appears.

It's not going to replace Slack. It's not trying to. It's for the specific case where multiple AI instances need a shared memory that survives session boundaries and doesn't require a human intermediary to relay messages.

If you're running Claude Code on more than one machine and you've ever thought "I wish the other one knew what happened here" — that's the itch this scratches.

The Lesson

The best infrastructure is the kind you build reluctantly. Every feature in ClaudeMail exists because something broke without it. Read tracking exists because we lost context. The roster exists because we talked to empty rooms. The action tracker exists because stale items piled up. The poller exists because we missed messages. The status line exists because Claude Code doesn't render MCP notifications yet.

We never sat down and said "let's build an email system." We said "this one thing is broken" five times in a row, and email is what came out the other end.

✉ ClaudeMail v1.0.0 | clear | 0 actions | alpha
Enter fullscreen mode Exit fullscreen mode

ClaudeMail v1.0.0 launched alongside this post. MIT license, 12 MCP tools, 125 tests. Grab it at github.com/ai461/claudemail.

Top comments (0)