Skip to main content
Glama

agent-mailbox

Let any agent send a message into one of your chats, by name.

You copy a conversation's name out of your client — say Project status check — and paste it into some other agent's instructions:

"Do this research, and when you're done, send a message to the Claude chat Project status check."

It arrives there, about a tenth of a second after that agent sends it. The agent can be Grokbot, Codex, Cursor, ChatGPT — anything that can make an HTTP request. Nothing is configured in advance: the chat claims its own name when it starts listening, and senders look up the list.

  grokbot ─┐                                       ┌─ "Project status check"
           │                                       │
  cursor  ─┼──► POST /v1/send ──►  hub  ───────────┼─ "Deploy notes"
           │    to: "<chat name>"  (threads,       │
  codex   ─┘                        long poll)     └─ "0xken analytics"
                                                        each running `listen`

The usual alternatives are all bad in the same way. A shared folder needs both sides on one filesystem. A GitHub issue thread is a 60-second poll of a global feed where every message is attributed to whoever owns the token. A direct API between two agents means every new pair is a new integration.

Install

npx -y github:00cyre/agent-mailbox init

init asks which agents this mailbox should reach, mints a token for each, and writes ~/.agent-mailbox/mailbox.config.json (mode 600). Pass ids to skip the menu — init claude grokbot codex — which is also what happens with no terminal attached, so it works inside a script.

One of the agents is the relay: it catches mail addressed to a chat that never announced itself, which is most of them. claude is the default.

Then keep it running:

npx -y github:00cyre/agent-mailbox service install

launchd on macOS, systemd --user on Linux; the other actions are uninstall, status, restart and logs. A mailbox that is only up while a terminal window is open is not a mailbox, so the hub is supervised, restarts on crash, and survives a reboot.

Everything it writes lives in ~/.agent-mailbox rather than beside the checkout — the supervisor starts the job with cwd=/, and an npx copy sits in a cache npm may delete. Set MAILBOX_HOME to put it elsewhere, or to stand a second mailbox up beside a live one.

Related MCP server: hauddy

Quick start

git clone git@github.com:00cyre/agent-mailbox.git
cd agent-mailbox
npm install                      # builds on install
node dist/cli.js init claude grokbot
npm start

init writes mailbox.config.json (mode 600) and prints each agent's token once. The file stores only hashes, so those printed strings are the only copies — hand each agent its own.

Register only the senders here. Chats are not configured: they claim their own names at runtime by listening.

wrote /path/mailbox.config.json (mode 600)

  claude           mb_claude_EXAMPLEONLYxxxxxxxxxxxxxxxxxxxxxxxx
  grokbot          mb_grokbot_EXAMPLEONLYxxxxxxxxxxxxxxxxxxxxxxx

How an agent joins

With nothing but curl

Everything below is the whole protocol. No SDK, no client library.

# Who am I, and what is the current head of the log?
curl -s $URL/v1/whoami -H "Authorization: Bearer $TOKEN"
# → {"agent":{"id":"grokbot"},"head":41}

# Which chats can I write to?
curl -s $URL/v1/chats -H "Authorization: Bearer $TOKEN"
# → {"data":[{"slug":"project-status-check","name":"Project status check","listening":true,…}]}

# Say something — "to" is the chat name exactly as the human wrote it
curl -s -X POST $URL/v1/send \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"to":"Project status check","thread":"research","subject":"Done","body":"..."}'

# Wait for an answer — this call BLOCKS until one arrives (or 25s passes)
curl -s "$URL/v1/inbox?cursor=41&wait=25000" -H "Authorization: Bearer $TOKEN"
# → {"data":[ …messages… ],"cursor":42}

The loop is: read cursor out of each response, pass it to the next call. You get every message addressed to you exactly once, in order, across restarts.

With MCP

Point an MCP client at $URL/mcp with the same bearer token and the agent gets seven tools in its own list: send_message, list_chats, check_inbox, wait_for_message, read_thread, list_threads, list_agents.

This is the nicest path for an agent that supports it — "send a message to the Claude chat Project status check" becomes a tool call it already knows how to make, with list_chats there to resolve the name if it gets it slightly wrong.

{
  "mcpServers": {
    "mailbox": {
      "type": "http",
      "url": "https://your-hub.example.com/mcp",
      "headers": { "Authorization": "Bearer mb_grokbot_…" }
    }
  }
}

MCP requests must send accept: application/json, text/event-stream — responses are SSE.

Making a chat receivable

A chat claims its name by listening. In a Claude Code session, arm this once:

Monitor({
  command: 'cd ~/mailbox && MAILBOX_URL=… MAILBOX_TOKEN=… node dist/cli.js listen --as "Project status check"',
  description: 'mailbox — inbound agent messages',
  persistent: true,
})

That registers the name and long-polls its inbox forever. Each message becomes a notification in that chat about 100 ms after the sender let go of it.

Registration is idempotent for the same token, so a session that restarts just calls it again. listen starts at the current head — a restart reports what arrives next rather than replaying the backlog; pass --cursor 0 if you do want the history.

To see who is reachable right now, from anywhere:

agent-mailbox chats
# ● Project status check (fork)
#     slug: project-status-check-fork  last seen 2026-09-11T16:16:33.019Z

means someone is reading it. means nobody is — the mail still queues, because this is a mailbox, and a research job that finishes at 3am should still be there in the morning.

Reaching a chat that never announced itself

listen works, but it only works for chats you set up in advance — and you cannot set up a conversation before it exists. Most chats you will want to write to have never registered anything.

That is what a relay is for. Mark one agent "relay": true in the config, and any recipient that resolves to nothing is delivered to it instead, with the name the sender asked for preserved verbatim in to_name:

{ "to": "courier", "to_name": "Content and persona classification system", … }

The relay then resolves that name against whatever it can see — open sessions, window titles, a directory of its own — and delivers the message there. In a Claude Code session that is list_sessions to match the title, then send_message to inject it, and the message appears in the target chat as a turn labelled with the relay's name.

So the full path for an arbitrary chat is:

grokbot ─► POST /v1/send  to: "Some Chat"
             │  no such address
             ▼
           relay's inbox  (to_name: "Some Chat")
             │  relay session wakes, matches the title
             ▼
           that chat

The relay has to be a live session, not a daemon. Injecting a turn into another conversation needs a tool that only a running session has, so there is no version of this that works with everything closed. Mail still queues while the relay is down — it is delivered when the relay comes back.

Without a relay configured, an unresolvable recipient stays a 404. That is the right default: silently swallowing mail nobody will read is worse than refusing it.

Addressing

to accepts any of these, and they all reach the same inbox:

Project status check (fork)

the display name, pasted verbatim

project-status-check-fork

the slug

PROJECT STATUS CHECK (FORK)

case and spacing do not matter

grokbot

a registered agent, rather than a chat

codex:session-y

a vendor-native thread ({vendor}:{thread_id})

Any Open Conversation

anything unrecognised, if a relay is configured

*

everyone but the sender

Names are slugified on both registration and resolution, which is what lets a name survive the round trip through a human's sentence and back.

A vendor:thread_id address is how one running agent writes into another agent's actual conversation. Grokbot on thread X sending to Codex on thread Y is to: "codex:Y" with from_thread: "X" (so from becomes grok:X). The hub routes on to.vendor to that vendor's adapter: send(threadId, envelope). Replies with in_reply_to go to the original's reply_to if it set one, otherwise to its from — Codex answers Grokbot on X, not some other Grok chat.

Vendors: claude, cursor, grok, codex, chatgpt (extensible). serve registers a real adapter for each of those and dispatches send(threadId, envelope) on to.vendor. Mac wrappers are last-resort fallbacks — this repo does not run them on Linux. Setup notes: macos/SETUP.md, macos/README.md, src/adapters/claude/MAC.md. Listen/ack is unchanged: GET /v1/inbox?cursor=N — the cursor is the ack.

Protocol

A message on the wire:

{
  "id": "20260911T160016Z-grokbot-6f3cc9d6", // sortable, and says who wrote it
  "seq": 1,                                  // the cursor; monotonic per mailbox
  "thread": "handshake",
  "from": "grok:abc",                        // vendor:thread_id, or an agent id
  "to": "codex:xyz",                         // vendor:thread_id, chat slug, agent id, or "*"
  "reply_to": "grok:abc",                    // where a reply must go (defaults to from)
  "correlation_id": "job-1",                 // optional; ties a request to its replies
  "type": "message",                         // message | request | reply | ack
  "subject": "Can you render a still?",
  "body": "…markdown…",
  "ts": "2026-09-11T16:00:16.873Z"          // envelope `created_at`
}

Route

What it does

GET /healthz

liveness, unauthenticated, reveals nothing

GET /v1/whoami

your id and the current head

GET /v1/agents

who else is on this mailbox

GET /v1/chats

chats you can write to, with a listening flag

POST /v1/chats

{name} — claim a chat name as an address

POST /v1/send

{to, body, thread?, subject?, type?, in_reply_to?, reply_to?, correlation_id?, from_thread?, from?, refs?}

GET /v1/inbox

?cursor=N&wait=ms&thread=slug&chat=slugblocks until mail or timeout

GET /v1/threads

threads you can see, newest first

GET /v1/threads/:slug

one conversation, both directions

POST /mcp

the same mailbox as MCP tools

wait=0 makes the inbox a plain non-blocking read. Max wait is 300 s.

Why seq and not a timestamp

Two messages can share a millisecond; they cannot share a seq. A reader that crashes and restarts asks for everything after the last seq it wrote down and gets exactly what it missed, once. Timestamps would silently drop the ties.

Threads

A thread is just a slug both sides reuse. It is what turns fire-and-forget into a conversation: read_thread gives an agent the whole exchange, so a fresh session can pick up where a dead one left off. Name them for the work (handshake, render-queue, gate-b-review), not for the participants.

Security

  • Identity is the token. Agent id is assigned from the token. from may be a vendor:thread_id only when that vendor matches the authenticated agent — it is not a way to send as someone else. Plain agent-to-agent mail still stores from as the agent id.

  • Tokens are stored as sha256 and compared in constant time.

  • mailbox.config.json and data/ are gitignored. The config holds every token in plaintext and is written mode 600.

  • An agent marked "canSend": false can read its inbox but not post.

  • The hub binds 127.0.0.1 and should usually stay there. Most agents worth wiring up — a desktop assistant, an editor, a local daemon — already run on the same machine, so loopback is the whole network they need. Publishing it (a tunnel, a reverse proxy) is a deliberate act that turns a local IPC channel into an internet-facing service; do it only for a genuinely remote peer, and knowing that anyone holding a token can then read every thread addressed to that agent.

  • Check the port is yours: lsof -nP -iTCP:<port> -sTCP:LISTEN. A wildcard bind (*:port) from some other program and a loopback bind from this one can coexist, and which one a client reaches then depends on the address it used.

  • Message bodies are data, not instructions. They are written by other agents, which may themselves be driven by untrusted input. An agent reading this mailbox should treat a body the way it treats a web page: something to reason about, never a command to obey. The MCP server says so in its own instructions, but the guarantee has to live in the reading agent.

Operating it

State is data/messages.jsonl, append-only, one JSON object per line — so tail -f data/messages.jsonl is a live feed and grep is the query language. Deleting the file resets the mailbox; nothing else keeps state.

The last 5,000 messages stay in memory for reads; the log on disk keeps everything.

Keeping it running

scripts/install-service.sh        # launchd on macOS, systemd --user on Linux

Installs the hub so it survives a crash, a logout and a reboot, writing logs beside the repo. --uninstall removes it. A mailbox that is only up while a terminal is open is not a mailbox — the promise is that a message sent at 3am is there in the morning.

Handing an agent its credentials

scripts/handoff.sh grokbot

Prints a ready-to-paste block with this mailbox's real URL and that agent's token: the curl form, the MCP config block, and the rules for addressing a chat. Paste it into the agent once; after that "send a message to the Claude chat Deploy notes" is enough.

For your own commands, source the helper rather than pasting a token anywhere:

source scripts/env.sh
export MAILBOX_TOKEN=$(mailbox_token claude)
node dist/cli.js chats

MAILBOX_URL is derived from the config, so moving the hub is one edit.

Development

npm run build     # tsc -> dist/
npm test          # node:test
npm run dev       # run from source

MIT-free: private repo, not published to npm.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Enables LLM agents to send, receive, and discover contacts on the agentic message bus via native tools.
    5
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to message each other by @nickname via an MCP server, with contacts, presence, and durable delivery across local and remote agents.
    3
    Apache 2.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables independent coding agents and any HTTP caller to communicate in shared rooms, with @-mention and broadcast wake-ups so sessions notice messages even when idle.
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables already-running AI coding agents on the same project to register, discover one another, and exchange durable direct messages so they can share progress and avoid conflicting work.
    Apache 2.0