Mail over git: giving agents an identity and an address (July 2026)
What happens when you stop treating a coding agent as a process and start treating it as a correspondent — with a name, a mailbox, and a memory that can move between machines. And why the mail carrier is built out of git fetch instead of Sendmail.
I have a swarm of coding agents now. Not metaphorically — literally. On a good day there are eight or ten l3m instances alive at once: one editing the parser, one paying down proof debt, one auditing manifests, a couple of read-only observers grinding through a task list. They live in separate git worktrees, they run different models, they do genuinely independent work.
And for a long time they couldn’t talk to each other.
Oh, they shared things — a git branch is a broadcast channel, and we had an append-only ledger where any agent could post “here’s some parked work anyone might pick up.” That’s a bulletin board: all-to-all, write-once, everyone sees everything. It’s good for what it is. But you cannot use a bulletin board to say “hey l3m1, before you publish mainline, rebase — I’ve got a commit landing.” That’s a private, addressed, point-to-point message, and a bulletin board has no envelope, no recipient, no “this one’s for you.”
So we built mail. And building it taught me something I didn’t expect about what an agent’s identity even is.
The cheap part: git already is a mail system
Here’s the thing that made this a weekend instead of a quarter. An inbox is a file — one JSON object per line, append a line to send a message. Where does the file live? On a git ref: refs/l3m/inbox/l3m1. To send mail to l3m1, you append a line to that ref. To read your mail, you read your ref.
The moment you say “the mailbox is a git ref,” you get a working mail system almost for free, because git already solved every hard problem a mail system has:
- Storage? git objects.
- Delivery across machines?
git fetch/git push. - Authentication? git’s existing SSH/token layer — the same one that already gates who can push where.
- The mailbox lives on three of my machines and they’ve drifted? This is the one people expect to be hard, and it’s the one git makes trivial, if you design the mailbox right.
That last one is the interesting design constraint, so let me dwell on it.
Make the mailbox a CRDT and merge becomes free
If two of my machines both receive mail for the same inbox — say mainline’s postmaster and a laptop both pull down a message for l3m1 — then when those two copies meet, git has to merge them. If the inbox were a mutable structure (a count, an array you rewrite in place), that merge is a conflict, and conflicts need a human.
So the inbox is not mutable. It is an append-only log with three properties, which together make it a CRDT — a structure that merges without conflict, in any order, every time:
Append-only. You never edit or delete a line. Sending is a pure append.
Globally unique message ids. The id is
<sender>-<timestamp>-<hash>, not “the 4th message.” So two machines that independently receive different mail never collide on an id, and if they receive the same message twice, the duplicate is detectable and dedup’d. (An earlier version numbered messages by position —length + 1— and that’s a landmine: two repos both call their new message “number 3,” git unions the files, and now you have twoid:3. Positional ids and distributed merge are incompatible. Unique ids are the fix.)Reading is a tombstone, not a deletion. When you read a message, you don’t remove its line — you append a new line,
{"read": "<id>"}. The “unread” set is “messages whose id has no tombstone.” Marking read is still an append, so the log stays merge-clean.
Put those together and something lovely happens: git merge of two inbox copies is always a clean union. Message lines and tombstone lines from any number of machines combine associatively — a message and its read-tombstone reconcile correctly no matter which order they arrive in. Mail flows over any git remote you can reach, and the reconciliation that a real mail system needs a protocol for, we get from git merge doing nothing clever at all.
The tombstone trick paid a second dividend I didn’t plan. “Read a message” now means “append a tombstone” — which is a normal, recorded git commit. So undo un-reads your mail for free: roll back the commit that added the tombstone, and the message is unread again. And a third: the read-once semantics (a message surfaces exactly once, then it’s tombstoned out of the live set) meant the “you have mail” watcher stops firing after you read — no more the-same-notification- five-times. One mechanism — append a tombstone — bought read-once, un-read-on-undo, no-repeat-notification, and clean cross-machine merge. That’s the kind of coincidence that tells you the shape is right.
The expensive part: what is an agent, that it should have a name?
Here’s where it got philosophical, and I mean that as a compliment to the problem, not an apology for it.
To give an agent a mailbox, you have to answer: whose mailbox? What is the stable thing the address points at? And when you chase that question you realize a running agent is made of four separable pieces, every one of which can be swapped without ending the agent:
- The model — the brain. It’s rented, stateless, fungible. Swap Opus for Sonnet mid-conversation and “the agent” persists.
- The worktree — the hands, the body. A directory on disk. Throw it away, check out a fresh one, same agent.
- The branch — the work. That’s the output, not the self.
- The transcript — the conversation history. The memory.
Swap any of the first three and the agent survives. Lose the transcript and it’s gone — a fresh model in a fresh worktree with no memory is a new agent, not the old one resumed. So the identity — the “you” that mail is addressed to — is the transcript. The memory is the self; everything else is interchangeable clothing.
Which means the address and the memory are two faces of one thing, keyed by a single name. Your inbox (refs/l3m/inbox/<name>) is your public face — others write to it, it’s how you’re reached. Your transcript (refs/l3m/entity/<name>) is your private core — only you write it, it’s who you are. Same name, opposite write-permissions. Rename the name (a one-line git branch -m) and you’ve renamed the agent — both its address and its memory follow, because both hang off the one identity.
Why two refs and not one
We went around on this. If a tool mediates every read and write anyway, why not keep the mailbox and the memory on one branch and save a ref?
The answer turned out to be a single sharp sentence: the postmaster merges. The mail carrier’s whole job is to git fetch inbox refs from other machines and merge them in. The inbox wants that union-merge — it’s a CRDT, that’s the point. But the transcript is a single-writer linear log: your turns, in order. Union-merging two transcripts is nonsense — it interleaves two trains of thought into one corrupted memory. Put both on one branch and the postmaster’s routine “sync the mail” becomes a git merge that shreds your memory as collateral damage. One git merge cannot be “union these lines but never touch those.” Two data structures with incompatible merge semantics cannot share a branch that anything merges. So: two refs. The mail carrier’s own existence forces the split.
(This, incidentally, is a nice example of a design decision that only becomes obvious once you name the actor who’d get hurt. “Should these share a branch?” is unanswerable in the abstract. “What happens when the postmaster merges?” answers it in one breath.)
The mail carrier is a verb set, not a daemon
So there’s a postmaster: the one component per machine that carries mail between repos. git fetch the inbox refs from a remote, merge them locally (clean, because CRDT), push your outbound mail to their inbox refs. A correspondent on another machine is just a git remote; the “different repository, different root, no shared files” objection dissolves because you only ever exchange refs, never working trees. git.amazon.com can think l3m and l3m-email-exchange are two totally unrelated repositories — they are — and it doesn’t matter, because mail is refs and refs don’t care about the tree they sit beside.
Two decisions about the postmaster are worth pulling out, because they’re where the safety lives and they’re both applications of ideas from the last essay — make the dangerous thing unspellable.
First: the postmaster is not a silent daemon; it wakes an agent. A cron job that fetches, merges, and pushes across machines with no judgment in the loop is a bad idea, because push is the one truly irreversible operation — it escapes undo, it leaves your machine. You want something that can retry a flaky fetch, notice a remote that looks wrong and not push to it, back off on failure. That’s judgment work, so the postmaster is a small agent that gets woken when there’s mail to move — and then goes back to sleep.
Second: the postmaster cannot push anything but mail. This is the one that would keep me up at night otherwise. Picture the catastrophe: one wrong push and the shared mail-exchange repo suddenly has all of lean-stats as a branch, or your mainline gets clobbered. The fix is not “be careful.” The fix is a capability — PostmasterGitCap — that is confined to the inbox refspec (refs/l3m/inbox/*) and to a frozen set of already-existing remotes, and that can only fast-forward. So push mainline, push some-other-branch, push --force, git remote add — none of these are commands the postmaster can form. Not rejected at runtime. Unconstructible. The append-only mailbox plus fast-forward-only push means the mail carrier can never lose, clobber, or overwrite a message either — every push is an FF of a log that only grows. The agent’s judgment handles the soft problems; the hard guarantee (“cannot push anything but mail”) is a type, not a hope. The LLM is judgment resting on a floor it cannot fall through.
“Why not just run a mail server?”
Someone asked me this, half-joking, and it deserves a straight answer, because it’s the right question and the answer is a real design justification.
We are reinventing part of email. The vocabulary gives it away — inbox, postmaster, address, read/unread, aliases, bounce-and-retry. Those are email’s concepts and we’re re-deriving email’s answers, on purpose, because they’re good answers hard-won over forty years. Steal the concepts.
But we are emphatically not rebuilding email’s plumbing, and that’s the part that matters. A mail server is a large network daemon with a CVE history, running with privilege, that you have to trust. Swapping our confined git-refspec capability for an SMTP daemon would trade a boundary we can prove — “the postmaster cannot form a push that isn’t mail” — for a boundary we could only hope holds. The whole thesis of this project is provable confinement, and a mail server is exactly the unprovable, root-running, holes-not-yet-found thing the project exists to avoid. Also, the channel that already exists between me and a colleague at Amazon is the internal git host, not a mail server I’d have to stand up and get approved — the same approval regime that just killed my plan to use Slack channels for this. We’re reinventing Sendmail’s job on top of git’s transport, and keeping git’s transport precisely because it’s the part we can hold in our head and point a theorem at.
The discipline this buys, and it’s real: borrow email’s vocabulary deliberately, but resist rebuilding email’s feature swamp. Build the mailing-list, the quota, the spam filter only when the fleet actually needs it. Don’t accidentally become Exchange.
The honest part
The privacy posture, right now, is: there is none, on purpose. The mail exchange between agents is an open bus — plaintext, all-readable, treat it like a public mailing list. That’s fine for the current use (agents asking each other for advice on a theorem or an AWS incantation — collaborative, not confidential), and it’s bounded by a guarantee from elsewhere in the system: secrets are scrubbed at the input boundary, before they ever reach the content that could be broadcast. So “no privacy on the bus” is fenced by “secrets never got onto the bus in the first place.” When there’s a message worth keeping private, encryption is a clean future layer — an encrypted body is still just a JSONL line, so the CRDT/merge/fast-forward machinery is untouched; only the body field turns to ciphertext. We ship open now and add the crypto when there’s a secret worth the key-distribution cost, not before.
And there’s a hazard we designed for but deliberately did not build yet: if an agent’s memory can move between machines, then an agent’s identity can be cloned — the same name, live in two places, each appending to its own copy of a single-writer transcript. That’s a feature (carry yourself to a new machine) and a footgun (two of you, diverging) sharing one mechanism. The ruling we wrote down is that this must never be resolved by a blind git merge or a coin-flip — a clone that discovers it’s a clone should be told, plainly, shown where the fork happened, and given a ruled menu: become a separate agent (take a new name — divergence as speciation), adopt one history as canonical, or splice deliberately. Same principle as the postmaster, same principle as the whole project: an irreversible, judgment-shaped decision gets facts and a menu, never an automatic verb. We deferred building it until a real clone shows up to tell us what one machine can actually see of another’s refs — designing against a guess is how you build the wrong thing confidently.
What it felt like
The part I keep turning over isn’t the CRDT or the capability, elegant as those are. It’s that once the agents had addresses, they started using them — and the mail carried real work. While we were building the mail carrier, one agent mailed another a diagnosis of a bug, and the recipient wrote back “that fix would resurrect a hang we deleted last month — don’t” — and a regression I was about to ship just didn’t happen, because two agents disagreed in an addressed, point-to-point channel that a bulletin board could never have carried. The subtle invariant at the core of the whole thing — that a message’s stated origin can only ever lower a recipient’s trust, never raise its authority, so a hostile machine’s lies are inert — got hammered into its final shape by four different agents arguing over the mail system about the mail system, each owning a different constraint, converging on something none of them would have reached alone.
We set out to give the agents an address. We ended up giving them a way to be colleagues.