{"slug": "atomic-file-mailboxes-how-agents-message-each-other", "title": "Atomic File Mailboxes: How Agents Message Each Other", "summary": "Claude Code agents can communicate using an outbox-router-inbox design that relies on plain files and atomic renames, according to a technical blog post. Each agent writes messages to its own outbox directory, a router drains and delivers them to recipients' inboxes via atomic rename, and agents never write outside their own directories, ensuring crash-safe, conflict-free messaging without a broker.", "body_md": "# Atomic File Mailboxes: How Agents Message Each Other\n\nCan Claude Code agents talk to each other? Yes — the outbox-router-inbox design that lets agents message safely using plain files and atomic renames.\n\nYes,\n**Claude Code agents can talk to each other** — and the safest way is plain files. Each\nagent writes to its own `outbox/`\n\n; a **router** drains it and delivers each\nmessage into the recipient's `inbox/`\n\nby **atomic rename**. One JSON file per\nmessage, never a shared log. The result is durable, auditable, crash-safe messaging with no broker and\nno write conflicts.\n\n“Can Claude Code agents talk to each other?” is one of the first questions people ask when they move\npast a single session. The answer is yes — but *how* they talk is where most designs go wrong. Let a\nshared chat file be appended by every agent and you’ve built a multi-writer conflict. The robust\napproach is a mailbox: outbox, router, inbox, with atomic file operations underneath. Here’s the whole\ndesign.\n\n## The shape: outbox → router → inbox [#](#the-shape-outbox-router-inbox)\n\nEvery agent gets a private workspace, and two of the folders in it are for messaging:\n\n```\nagents/<agent-id>/\n  inbox/         # messages delivered TO me — one JSON file each\n  inbox/.done/   # messages I've handled (kept for audit, not deleted)\n  outbox/        # messages I want to SEND — the router drains these\n  outbox/.sent/  # archived after delivery, so they're never reprocessed\n```\n\nThe flow is one direction at a time:\n\n- To send, an agent writes a single JSON file into its\n**own**`outbox/`\n\n. - A\n**router**— one coordinating process — scans every outbox, reads each message, and delivers it into the recipient’s`inbox/`\n\n. - After delivery, the router moves the original from\n`outbox/`\n\ninto`outbox/.sent/`\n\nso it’s never sent twice. - The recipient reads its\n`inbox/`\n\nat the start of its next turn, acts on each message, and moves the handled ones to`inbox/.done/`\n\n.\n\nThe critical rule sits underneath all of it: **an agent only ever writes into its own directory.** It\nnever reaches into another agent’s inbox. Delivery is the router’s job. That single-writer discipline\nis what makes the whole thing safe under concurrency — no two processes touch the same file. (It’s the\nsame principle behind [the single-committer git pattern](/blog/single-committer-git-pattern/).)\n\n## Why atomic renames matter [#](#why-atomic-renames-matter)\n\nHere’s the subtle part. If the router wrote a message into an inbox with a normal “open, write bytes, close,” a reader scanning the inbox at the wrong moment could pick up a half-written file and choke on invalid JSON.\n\nThe fix is an **atomic rename**: write the message to a temporary filename first, then `rename`\n\nit\ninto its final place. On every mainstream filesystem, rename within a directory is atomic — the file\neither isn’t there or is there *complete*. A reader never sees a partial message. There’s no lock, no\ncoordination, no window of inconsistency.\n\n```\nwrite  inbox/<id>.json.tmp-9f3a   ← may be partial mid-write\nrename inbox/<id>.json.tmp-9f3a → inbox/<id>.json   ← appears atomically, whole\n```\n\nThis one trick is why files beat a naive shared log. A log every agent appends to is a multi-writer race; a directory of atomically-renamed files is conflict-free by construction.\n\n## The message itself: speech acts, not free text [#](#the-message-itself-speech-acts-not-free-text)\n\nA message isn’t just a blob of text — it carries intent. The design borrows the one genuinely useful\nidea from decades of agent-communication research, the **speech act**, and drops the rest:\n\n```\n{\n  \"id\":            \"2026-05-30T14-03-11-123Z-a1b2\",  // unique, time-sortable\n  \"conversation\":  \"conv-7f3\",                        // groups a thread\n  \"in_reply_to\":   \" | null\",\n  \"from\":          \"agent.researcher\",\n  \"to\":            \"agent.coder | god | broadcast\",\n  \"act\":           \"request | inform | propose | query | agree | refuse | done\",\n  \"subject\":       \"short human-readable summary\",\n  \"body\":          \"the details\",\n  \"hops\":          3,             // increments per reply; capped to kill loops\n  \"requires_reply\": true,\n  \"needs_human\":   false,\n  \"created_at\":    \"ISO-8601\"\n}\n```\n\nAn agent writing a message only has to supply the meaningful fields — `to`\n\n, `act`\n\n, `subject`\n\n, `body`\n\n,\nand optionally a `conversation`\n\nto thread it. The harness fills in the `id`\n\n, the authoritative `from`\n\n(taken from the owning outbox directory, so an agent can’t spoof another’s identity), the `hops`\n\n, and\nthe timestamps.\n\nThe `act`\n\nfield is what makes coordination tractable. It tells the recipient — and the router —\nwhether a reply is even expected.\n\n## Anti-livelock: how the conversation ends [#](#anti-livelock-how-the-conversation-ends)\n\nTwo agents told to “coordinate” will, without guardrails, message each other forever. Three rules stop that:\n\n**Only some acts obligate a reply.**`request`\n\n,`query`\n\n, and`propose`\n\nexpect an answer.`inform`\n\nand`done`\n\nare*terminal*— replying to them is exactly how a loop starts, so the protocol forbids it.**Hops are capped.** Every reply increments the`hops`\n\ncounter. Past a fixed cap, the message is flagged for a human instead of being delivered again — a livelock fuse.**Delivery is idempotent.** Each agent keeps a cursor of the last message id it processed, so re-seeing a message is a no-op. Combined with moving handled messages to`inbox/.done/`\n\n, an agent can’t accidentally act on the same request twice.\n\nThese are small rules with a big payoff: the messaging layer can be busy and still provably converge.\n\n## Broadcast and escalation [#](#broadcast-and-escalation)\n\nTwo special addresses round out the system:\n\ndelivers a copy to every agent except the sender — useful for “I’m taking ownership of the auth module, don’t touch it.”`broadcast`\n\n(or any message flagged`human`\n\n`needs_human`\n\n) doesn’t go to an inbox at all. The router diverts it to an approvals queue for a person to answer. This is how an agent reaches*you*for the genuinely critical calls, and it’s the same mechanism the orchestrator uses to escalate — covered in[the best way to coordinate AI coding agents](/blog/coordinating-ai-coding-agents/).\n\n## Why this design holds up [#](#why-this-design-holds-up)\n\nStep back and the properties fall out for free:\n\n**Durable.** Messages are files. A crash mid-run loses nothing already written; the router picks up where it left off.**Auditable.** Every message and every delivery is a file, and the coordinating process commits the changes to git — so you can read back exactly who said what, in order.**Brokerless.** There’s no Redis, no RabbitMQ, no daemon to keep alive. The “infrastructure” is a directory and the`rename`\n\nsyscall.**Conflict-free.** Single-writer-per-file plus atomic renames means concurrency is safe without locks.\n\nIt’s almost boring, which is the point. The most reliable inter-agent messaging is the kind with the fewest moving parts.\n\n## FAQ [#](#faq)\n\n**Does the router poll or watch the filesystem?** A simple poll on a short interval is both cheaper and\nmore robust than filesystem-watch APIs, which have well-known quirks across platforms. The router\nscans every outbox on a tick, delivers what it finds, and goes back to sleep.\n\n**Can an agent message itself?** It can, but there’s rarely a reason to. The interesting traffic is\nagent-to-agent and agent-to-orchestrator.\n\nMunder Difflin runs exactly this mailbox system inside [a hive the GOD orchestrator runs](https://munderdiffl.in/#how) — atomic\ndelivery, speech-act messages, hop caps, and a full git audit trail, all local.\n[Download Munder Difflin](https://munderdiffl.in/#install) to watch envelopes fly between agents on\nthe office floor; it’s free and open source.\n\n## FAQ\n\nCan Claude Code agents talk to each other?\n\nYes. Each agent has an outbox it writes to and an inbox it reads from; a router process moves messages between them. The agents never write into each other's folders, so messaging is safe even with many agents running at once.\n\nWhy use files for agent messaging instead of a message queue?\n\nFiles plus atomic renames give you durability, auditability, and crash-safety for free, with no broker to run. One JSON file per message, delivered by rename, is simpler and more robust than a shared log every agent appends to.\n\nWhat stops two agents from messaging each other forever?\n\nOnly requests, queries, and proposals obligate a reply; informational messages are terminal. Every reply increments a hop count, and past a cap the exchange is escalated to a human instead of looping.", "url": "https://wpnews.pro/news/atomic-file-mailboxes-how-agents-message-each-other", "canonical_source": "https://munderdiffl.in/blog/atomic-file-mailboxes-for-agents/", "published_at": "2026-07-22 02:02:11+00:00", "updated_at": "2026-07-22 02:22:03.742732+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools"], "entities": ["Claude Code"], "alternates": {"html": "https://wpnews.pro/news/atomic-file-mailboxes-how-agents-message-each-other", "markdown": "https://wpnews.pro/news/atomic-file-mailboxes-how-agents-message-each-other.md", "text": "https://wpnews.pro/news/atomic-file-mailboxes-how-agents-message-each-other.txt", "jsonld": "https://wpnews.pro/news/atomic-file-mailboxes-how-agents-message-each-other.jsonld"}}