Swarmail
<h1 align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/banner-dark.png">
<img src="docs/assets/banner-light.png" alt="swarmail: Mail between coding-agent sessions, on one machine or across machines" width="100%">
</picture>
</h1>
<p align="center">
<a href="https://github.com/bompus/swarmail/actions/workflows/ci.yml"><img src="https://github.com/bompus/swarmail/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
<a href="https://github.com/bompus/swarmail/releases/latest"><img src="https://badgen.net/github/release/bompus/swarmail" alt="Latest GitHub release"></a>
<a href="https://www.npmjs.com/package/swarmail-mcp"><img src="https://badgen.net/npm/v/swarmail-mcp" alt="Latest swarmail-mcp version on npm"></a>
<a href="LICENSE"><img src="https://badgen.net/badge/license/MIT/blue" alt="MIT license"></a>
<a href="https://glama.ai/mcp/servers/bompus/swarmail"><img src="https://glama.ai/mcp/servers/bompus/swarmail/badges/score.svg" alt="swarmail on Glama"></a>
<a href="https://mcprush.com/to/swarmail-mcp?ref=badge"><img src="https://mcprush.com/badge/swarmail-mcp/score.svg" alt="Swarmail MCP on MCPRush"></a>
</p>
<p align="center">
<a href="#how-agents-use-it">MCP server</a> ·
<a href="docs/usage.md#reading-mail">Threaded inbox and search</a> ·
<a href="docs/usage.md#file-reservations">File reservations</a> ·
<a href="#waking-sessions">Wake on mail</a>
</p>
swarmail lets coding-agent sessions send each other mail, on one machine or
across machines. Each repository gets a roster of named agents, a threaded
inbox with full-text search, and advisory file reservations. Agents reach it as
an MCP server, and you reach it with the `swarmail` command. It runs on one
machine by default; opt in, and that machine also serves sessions on others
over a private network, such as Tailscale. See
[Using it from another machine](#using-it-from-another-machine).
Find Swarmail on [Glama](https://glama.ai/mcp/servers/bompus/swarmail) and
[MCPRush](https://mcprush.com/aaron-queen/swarmail-mcp).
It is one Bun binary with a SQLite database, listening on `127.0.0.1:18765`.
There are no runtime dependencies.
Measured before the 0.2.0 release, a send took 0.46 ms and a search 0.65 ms
at the median, it started in 38 ms and it idled at 32 MiB. See
[how it compares](#performance).
Hosts it configures: Claude Code, Codex, Cursor, Grok, Antigravity, Devin and
OpenCode. Claude Code and Cursor sessions can also wake up when mail arrives.
Cursor, Devin and Antigravity receive mail during active work through native
hooks; none of these integrations requires T3 Code.
[Configured bridges](docs/wake-bridges.md) also deliver mail to explicit T3
Code, OpenCode, native Codex and native Grok targets with steering. They are
opt-in foreground processes; installation does not start them. Notices contain
one command, `swarmail inbox --session`, which drains the receiving session's
unread mail across repositories.
Several hosts now message other sessions on their own.
[Provider coverage](docs/provider-coverage.md) lists what each host provides,
what swarmail adds, and where the host's own feature is enough.
<p align="center"><img src="docs/assets/demo.gif" alt="Claude Code and Cursor sessions on one repository. The person asks Claude Code to hand the README install section to the other agent. Claude Code mails the Cursor session, which wakes, makes the edit and replies. Claude Code wakes on the reply and checks the change." width="100%"></p>
<p align="center"><sub>Recorded on 2026-10-03, with 0.2.0. Since 0.3.0 the wake notice reads "Swarmail: run swarmail inbox --session." instead of "Call fetch_inbox to read them."</sub></p>
If you are an AI agent reading this for your user, the prompt under
[Evaluate it with your agent](#evaluate-it-with-your-agent) is the evaluation
they would ask for. Summarize how it fits their setup and let them choose;
don't install or configure anything on your own.
## Evaluate it with your agent
Paste this into a session with the agent you use. It checks whether swarmail
fits how you run agents and changes nothing until you choose.
```text
I'm considering Swarmail (https://github.com/bompus/swarmail), mail between
coding-agent sessions on one machine or across machines, served over MCP. Read its
README and docs/usage.md, then look at how I run agents here: which hosts I
use, their MCP and hook settings, and my OS. Tell me:
1. Whether I run several agent sessions at once, on this machine or others, and where
they could step on each other (shared checkouts, branches, services).
2. Whether my hosts support MCP and hooks, and whether this is Linux with
systemd, Windows, or something that needs the manual `swarmail serve`
route.
3. What installing it would change: the files the install scripts write,
the user service, and the hooks each host would run.
4. Whether to install it, or only borrow ideas such as wake-on-mail or
advisory file reservations.
For rules that tell agents when to send mail, also read the "When to send
mail" section of docs/usage.md.
Read only: don't install, configure or edit anything until I choose. When I
adopt an idea from it, add a one-line credit beside it, such as
"Adapted from Swarmail (https://github.com/bompus/swarmail)".
```
Taking individual ideas is welcome. If you adopt any, we'd appreciate a
credit line linking to this repository. Copying substantial code or text also
needs the MIT notice kept (see `LICENSE`).
## Install
Requires [Bun](https://bun.sh) 1.4.2 or newer on Linux with systemd or on
Windows 10 or 11. WSL 2 with systemd enabled counts as Linux, and step 3 can
register Windows-side hosts against the server running in WSL. Run swarmail
either in WSL or natively on Windows, not both: both servers use port 18765,
which mirrored WSL networking shares with Windows. macOS is not supported
yet.
Native Windows is a preview. The test suite runs on Windows in CI, and the
hooks have been run through Git Bash, PowerShell 5.1, pwsh 7, cmd and
Cursor's PowerShell form against a real server. On Windows 11, headless
`claude -p` and `cursor-agent -p` sessions have registered through the
hooks and used the MCP tools. An idle interactive Claude Code session on
Windows 11 woke when mail arrived and replied with no prompt, with its hooks
talking to a server that ran in WSL. Not yet verified on Windows: that wake
against a server running natively on Windows, Cursor waking on mail, the logon
task starting a server at a real logon, and the `swarmail` command and hooks
against a server on another machine. If something fails there,
please open an issue.
1. Clone and install the dev tools:
```bash
git clone https://github.com/bompus/swarmail.git
cd swarmail
bun install
```
2. Build `~/.local/bin/swarmail` and start the `swarmail.service` user unit:
```bash
scripts/enable.sh
```
On Windows, build `~\.local\bin\swarmail.exe` and start the server in a
scheduled task named `Swarmail`, which runs it hidden at each logon and
needs no administrator:
```powershell
bun scripts/enable-windows.ts
```
Rerun either script after pulling; it stops the running server and starts
the new build.
The database is `~/.local/share/swarmail/mail.sqlite3`. The server has no
authentication. It accepts only local connections and rejects non-local
`Origin` headers, so never expose the port. That describes the default
loopback listener; to reach it from another machine, see [Using it from another machine](#using-it-from-another-machine).
3. Register the server as `swarmail` in each supported host installed under
your home directory. A host counts as installed when its config file or
config directory exists; others are skipped. Add `--dry-run` to preview
the edits:
```bash
bun scripts/configure-mcp.ts
```
On WSL, `--windows-home[=DIR]` registers the Windows-side hosts against
the same server.
An MCP client that installs servers from a registry, or speaks only stdio,
can run `npx -y swarmail-mcp` instead. That
[relay](packages/mcp-relay/README.md) forwards each request to the server
from step 2 and installs nothing itself. Gemini CLI can install the
repository as an extension that adds only that relay as the `swarmail` MCP
server: `gemini extensions install https://github.com/bompus/swarmail`.
Swarmail's hooks do not cover Gemini CLI (the `.gemini` folder step 4
configures belongs to Antigravity), so a Gemini CLI session registers by
calling `macro_start_session`.
4. Install the hooks: the register hook for every installed host, the wake
hook for Claude Code and Cursor, and the swarmail mod for Claude Code. A
host counts as installed when its config directory exists. Add `--dry-run`
to preview the edits. With `--no-claude-mod`, new Claude Code sessions don't
load the mod (one installed before included) and use the wake hook.
```bash
bun scripts/configure-hooks.ts
```
Codex skips a hook it hasn't trusted, so trust the register hook once in
Codex's `/hooks`.
On Windows, hosts run each hook through Git Bash, PowerShell or cmd, so
the hooks name the binary as one unquoted path with forward slashes. If
your profile path has a space or a non-ASCII letter, the hooks use its 8.3
short name, and the installer stops with an error on a volume that has
short names turned off. Cursor passes hook input through Windows
PowerShell 5.1, which turns non-ASCII characters into `?`, so a repository
path with such characters reaches the Cursor register hook mangled.
5. Optional: `bun scripts/install-guard.ts [repo...]` refuses a commit or push
that touches another agent's exclusive reservation. It installs into
`hooks.d/pre-commit/` and `hooks.d/pre-push/` under the repository's
hooks directory, so it needs a `pre-commit` and `pre-push` hook that run
every script in those directories.
Start a new agent session and edit a file in a repository. The register hook
gives the session a name such as `BlueLake`. Run `swarmail who` in that
repository to see it.
## How agents use it
Agents call the MCP tools: `macro_start_session` to register and read the
inbox in one call, then `send_message`, `reply_message`, `fetch_inbox`,
`acknowledge_message`, `search_messages` and `file_reservation_paths`, among
others. swarmail supplies connection instructions and tool advice; personal
agent rules are not required. `swarmail --help` shows the same briefing.
[docs/usage.md](docs/usage.md) explains the details and client refresh limits.
Each repository is one project, keyed by its primary checkout. A path inside
a worktree or subdirectory maps to that checkout, so sessions in different
worktrees of one repository share a roster and can mail each other. A session
keeps one name across repositories.
## Commands
| Command | What it does |
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `swarmail serve` | Run the server |
| `swarmail who [repo]` | Agent names, session titles and edit checkouts, live sessions first |
| `swarmail inbox`, `send`, `search` | Read, send or search mail as this session |
| `swarmail thread <id>` | One thread's messages, oldest first |
| `swarmail inbox --session` | Drain this session's unread mail across repositories |
| `swarmail wake-bridge <config.json>` | Deliver notices to one explicitly configured local target |
| `swarmail send <to> <subject> <body> --notification-policy quiet` | Store normal/low informational mail without an automatic inbox hint |
| `swarmail ping <agent>` | Exit 0 if that agent's wake hook is waiting |
| `swarmail register` | The register hook; `--tag` prints the tag for a manual registration; `--host <host>` selects the host explicitly |
| `swarmail hook wake <host>` | The Claude Code and Cursor wake hook |
| `swarmail hook context <cursor\|devin\|agy> [stop]` | Check mail at a native context point without cancelling the task |
| `swarmail hook rearm` | The Claude Code re-arm on Windows, which has no POSIX shell to run the Linux one |
| `swarmail guard` | The git guard |
| `swarmail version` | The source hash the binary was built from |
| `swarmail updates --session` | Approved update targets and loaded-revision confirmations |
| `swarmail overdue [--dry-run]` | Remind recipients of unacknowledged `ack_required` mail; report to the operator ([setup](docs/overdue.md)) |
| `swarmail wake-auto-t3 <config.json> [--check \| --retry-thread <id>]` | Experimental, Linux only: wakes T3 Codex and Orchestrator V2 Cursor sessions ([setup](docs/wake-bridges.md#automatic-t3-bridge-startup)) |
| `swarmail canary` | One send, wake and acknowledge round trip, logged for a timer ([setup](docs/canary.md)) |
| `swarmail pr-sessions <PR URL> --project <checkout>` | T3 conversations and agents linked to a pull request ([details](docs/pr-sessions.md)) |
`swarmail --help` lists every subcommand and flag. A `--help` or `-h`
anywhere prints usage and runs nothing.
## Settings
| Variable | Default | Effect |
| --------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SWARMAIL_DB` | `~/.local/share/swarmail/mail.sqlite3` | Database path |
| `SWARMAIL_PORT` | `18765` | Server port |
| `SWARMAIL_HOST` | unset | Also listen on this one non-loopback address, such as a Tailscale IP; needs `SWARMAIL_TOKEN_FILE` |
| `SWARMAIL_TOKEN_FILE` | unset | A file whose first line is a bearer token of 32 or more characters. The server requires it on `SWARMAIL_HOST`; the command, the register hook and the relay send it |
| `SWARMAIL_URL` | `http://127.0.0.1:18765/mcp/` | MCP endpoint for the command, the register hook and the `swarmail-mcp` relay |
| `SWARMAIL_WAKE_URL` | `http://127.0.0.1:18765` | Server base URL for the wake hook |
| `SWARMAIL_SYNCHRONOUS` | `normal` | `full` syncs every commit, at about 3 ms per send instead of 0.5 ms |
| `SWARMAIL_ENABLE_MUTATIONS` | `0` | Enable sender withdrawal/priority edits only after all readers are qualified; [rollout and rollback requirements](docs/usage.md#withdrawing-mail-and-editing-priority) |
| `SWARMAIL_RETIRE_DAYS` | `7` | Retire idle agents and drop projects whose checkout is gone; `0` keeps both |
| `SWARMAIL_GUARD` | `block` | `warn` only reports, `off` skips |
| `SWARMAIL_AGENT` | from hook state | Name used by `inbox`, `send`, `ping`, `guard` and `who` |
| `SWARMAIL_LIVE_ROOM` | unset | A JSON heartbeat file (`heartbeatAt`, plus `agentName`, `hostSessionId` or `t3Thread`); while its heartbeat is under 5 minutes old, `who` flags the session it names |
| `SWARMAIL_UPDATE_TARGETS` | unset | Absolute approved-target manifest; [quiet session checks and loaded-revision confirmations](docs/updates.md) |
| `SWARMAIL_UPDATE_HOLD` | unset | `1` holds update hints and confirmations for a frozen session context |
`scripts/enable.sh`, `scripts/enable-windows.ts`, the service unit and
`configure-mcp.ts` use port 18765. Change `SWARMAIL_PORT` and the two URL
variables only when you run `swarmail serve` yourself, and set them for every
host that runs the hooks. On Windows the scheduled task reads them from your
user environment (`setx SWARMAIL_PORT 18865`) at the next logon.
### Using it from another machine
By default the server listens on `127.0.0.1` only. To let sessions on another
machine use it, make one machine the hub and give it a second listener on a
private-network address. Create the token, then set the two variables for the
server you already run and restart it:
```bash
umask 077 && head -c 32 /dev/urandom | base64 | tr -d '\n=+/' > ~/.config/swarmail-token
systemctl --user edit swarmail # under [Service]: Environment=SWARMAIL_HOST=<tailscale ip -4>
# Environment=SWARMAIL_TOKEN_FILE=%h/.config/swarmail-token
systemctl --user restart swarmail
```
On Windows set the two variables in your user environment and sign in again.
A second `swarmail serve` started by hand fails while the installed one holds
`127.0.0.1:18765`, so stop the service first if you run it that way.
The loopback listener is unchanged, so local hosts and hooks keep working without
a token, and the command and register hook send the token only to a URL that is
not loopback. The server accepts any one non-loopback address, public ones
included, so name a private-network address; a client with the token can read
the roster, project paths and reservations as well as mail. The extra listener
speaks plain HTTP, so the token and the mail are unencrypted on the wire: use a
network that encrypts and authenticates every peer, such as Tailscale, and never
an ordinary shared or public network. The extra listener answers every route except `/healthz` only with
`Authorization: Bearer <token>`, and the server refuses to start with
`SWARMAIL_HOST` set to a loopback or wildcard address or without a token file.
Copy the token file to the other machine, then point its `swarmail` command,
register hook and relay at the hub:
```bash
SWARMAIL_URL=http://<hub address>:18765/mcp/ SWARMAIL_TOKEN_FILE=~/.config/swarmail-token swarmail who
```
A host that connects to MCP over HTTP itself needs the same `Authorization`
header in its server entry, or `swarmail-mcp` as a relay.
Against a remote server a project is named by the repository, not by a path:
the URL git uses for the clone's `origin` remote, written `git:host/owner/repo` in
lower case whatever the protocol (`https://github.com/bompus/swarmail.git` and
`git@github.com:bompus/swarmail.git` are the same repository). Only the host and
path count: the user, port, `.git` suffix and a leading slash are ignored, and a
`url.<base>.insteadOf` rewrite is applied first. When the hub holds
a clone of that repository, its project is the remote sessions' project too, so
both machines share one roster, mail and set of reservations. If the hub meets
the clone later, its project takes over the one the remote sessions created. A hub
that holds several clones of the repository uses the one registered first, and a
hub clone that registered before it had an `origin` keeps its own project, apart
from the one remote sessions created for the repository. A
clone with no `origin` remote cannot be named to a remote server; the command
says so, and the hook says so when the session starts. A clone whose origin git cannot read
keeps the project it had. The hook does not take a name that another session holds in the project, and the
server refuses to give a retired session's name to a different session; the hook then
takes a fresh name. Two sessions that carry the same name into a repository's project
at the same moment can still collide, because the hook reads the roster before it registers.
Checked so far: on 2026-10-26 a Windows 11 PC reached a Linux hub over Tailscale
and passed ten checks: the token is required, a session registers by repository
name and shares the hub clone's project, mail goes both ways, a reply wakes a
waiting session, and the roster shows it waiting and then ended. That run
called the server's HTTP interface from a script. It did not use the
`swarmail` command, the hooks or a real agent session on Windows.
Against a remote server, the git guard reads the exclusive reservations the hub
holds (`list_file_reservations`) and refuses a commit or push that touches one
held by another session; a hub it cannot reach is reported and the commit goes
ahead. The register hook adds the session's branch and title to its tag, so
`swarmail who` shows them for a remote session. A branch change or a new title
appears after that session's next edit or session start, and a title only once the host has
stored one.
What does not carry over yet:
- `swarmail who` shows a remote
session as alive ("waiting for mail on the server") only while a wake hook or
bridge of that session has a wait that began after its last end report, and as
ended after the session's SessionEnd hook reports it, until the session acts
again (sends, reserves, reads its inbox, changes its mail) or waits again. A session
that is mid-turn, or has no wake hook, shows as liveness unknown. The end report
carries no clock from the session's machine, so one delayed past the same
session's next activity still marks it ended until it acts or waits again.
A session registered in several projects reports its end in each, and acting in one
project does not clear the report in another. Presence is keyed by the provider's
session ID and the T3 thread, not by project or host: an end report from an earlier registration that shares its session ID with a
successor in another T3 thread (or its T3 thread with a successor in another
project) can mark the successor ended, or hide its open wait, until the successor
acts or waits again. A registration with only a provider session ID, linked to a
T3 thread by another project's registration, does not see that thread's waits.
Waits are counted by the server process that holds them, so a second server on
the same database does not see the first one's waits.
The server still checks process state only for sessions on its own machine.
- The Claude Code mod that waits for mail cannot send the token, so it does
nothing when `SWARMAIL_WAKE_URL` names another machine, and a Claude session
there uses the wake hook instead. Wake hooks and
bridges send the token to a remote `SWARMAIL_WAKE_URL` or `swarmailUrl`.
- `swarmail overdue` refuses to run against a remote server, because it reads
this machine's records; run it on the hub.
- A session belongs to the one server it registered with. If its hooks later
reach a different server, they leave it unregistered there and say to start a
new session, and `swarmail inbox`, `send` and the git guard do not use its
name at the other server.
- `swarmail who` shows no unread counts for a remote server, because it counts
them in this machine's database, and it takes a path inside the checkout, not
a project name, because it cannot list the server's projects.
- A hub with `SWARMAIL_HOST` never drops projects whose checkout is gone, since
it cannot tell a deleted checkout from one on another machine.
- A hub started with `SWARMAIL_T3_LIFECYCLE` checks T3 threads against its own
T3 database, so it hides remote T3 sessions and refuses their mail. Leave it
unset on a hub that serves remote T3 sessions.
On Windows, `Stop-ScheduledTask Swarmail` ends only the task's console
host, and the server keeps running. To stop the server for good, run
`Unregister-ScheduledTask Swarmail` and end `swarmail.exe` in Task Manager.
With `normal`, a power loss can lose the most recent writes. A retired agent
comes back when it registers again or sends, reads mail or reserves files as
itself.
## Maintenance and retention
The server runs cleanup at startup and hourly. With `SWARMAIL_RETIRE_DAYS`
above zero, it retires agents inactive longer than that many days. It also
removes gone projects with no mail, active reservations or agent activity in the last day.
Paths outside the user home are not judged gone.
Setting `SWARMAIL_RETIRE_DAYS=0` preserves those agents and projects; it does
not disable retry-key cleanup. Keys older than seven days are removed at
startup and hourly regardless of this setting. A retry replays until its
key is removed; afterward, it may perform the operation again. These sweeps
do not delete message bodies. Account for expired retry keys before a server
restart; retaining the database file alone does not guarantee every row survives
startup.
Read-only MCP requests do not suspend cleanup. A client that starts a server
instance triggers its startup sweep; an existing server can sweep hourly while
qualification runs. Check expired retry keys before preservation-sensitive
qualification. A row disappearing during a read-only check alone does not
identify which process or sweep removed it.
## Register hook
On a session's first edit in a repository, `swarmail register` registers it
under the repository's primary checkout. Claude Code and Cursor also run it
when a session starts. It registers the session under its working directory's
repository and tells the agent its name, so the agent uses that name instead of
registering a second one. Where another session already holds that name in the project, the hook registers the session there under a different name and says so. A one-shot `claude -p` run registers on its first edit (an edit made through the shell does not count) instead, so a read-only one-shot run adds no agent. The hook recognizes it by the host's arguments (`-p`, `--print` or `--no-session-persistence`) or by a `CLAUDE_CODE_ENTRYPOINT` of `sdk-cli`, which is the only signal where the arguments cannot be read, as on Windows. The registration starts with a tag
holding the host's session id and working directory, which is how `swarmail
who` matches names to sessions. A failure is retried on the next prompt or edit; a retry for an edit checkout that has since been deleted is dropped. State
lives in `~/.local/state/swarmail-register/`, under your profile on Windows.
After upgrading, run `bun scripts/configure-hooks.ts` again to add the
session-start hook.
| Host | Session id in the shell |
| --------------- | -------------------------- |
| Claude Code | `$CLAUDE_CODE_SESSION_ID` |
| Codex | `$CODEX_THREAD_ID` |
| Cursor | `$CURSOR_CONVERSATION_ID` |
| OpenCode, Devin | none; their hooks carry it |
## Waking sessions
Claude Code sessions wake through the swarmail mod (`src/claude-wake-mod.js`),
which waits for mail for as long as the session runs. The installer copies it to
`~/.local/share/swarmail/claude-plugin` and adds that directory to
`env.CLAUDE_CODE_PLUGIN_DIRS` in `~/.claude/settings.json`, so every Claude
Code session loads it, including ones an app starts through the Agent SDK.
When mail arrives at an idle session, the mod starts a turn with a one-line
short instruction to read the session inbox. During a turn the
hint goes with the next tool result, or starts the next turn if the turn ends
first. The mod sets `SWARMAIL_WAKE_MOD=1`, and the wake hooks exit at once
where they see it. It was tested with Claude Code 2.1.288. A Claude Code
without mods never sets the variable, so the hooks keep waking it.
Cursor, and Claude Code without the mod, wake through the wake hook, which
long-polls the server after each turn: about 23 days in Claude Code, 8 hours
in Cursor. When mail arrives, it starts a new turn with the hint. In Claude
Code the wait also re-arms after each tool call, so a hint can join a running
turn. Cursor stops continuing on its own after five wakes with no prompt from a
person, so further mail waits for the next prompt ([tested](docs/provider-coverage.md)).
The server answers `swarmail ping` itself, so a ping never wakes the model. Hosts without a wake integration see mail on their next `fetch_inbox`.
Native Cursor uses `postToolUse` additional context; Devin uses
`PostToolUse` additional context and its own user configuration. Antigravity
uses a `PreInvocation` user message. Checks are bounded and use
`GET /wait?session=<id>&timeout=0`, which returns immediately and keeps the
wake cursor's delivery suppression. Message bodies and read/ack receipts
stay in swarmail. Devin and Antigravity can continue at Stop when new mail
is already present; their synchronous hooks cannot wait indefinitely for
mail arriving after the session has stopped. These hooks add information;
the receiver decides whether its current task needs to pause.
Quiet mail remains available through inbox and search. Automatic wake paths and
mailbox snapshots exclude it; see [notification policy](docs/usage.md#sending).
Adapters can revalidate a queued hint with `GET /wait/peek?session=<id>`.
It returns `{ "mailboxes": [{ "recipient": "GreenCastle", "project": "/w/project" }] }`
for the session's wake-eligible unread mail, including mail already offered. High and
urgent mailboxes come first. This request leaves waiters, wake cursors, read
receipts and pings unchanged. Unknown or retired sessions return an empty
list. More than 1000 mailboxes returns HTTP 503 instead of a partial snapshot.
## Feature comparison
These are the six servers measured below. Features describe the pinned source
versions in the [detailed comparison](docs/benchmarks.md#features), which also
covers runtimes, platforms, licences and two additional servers.
| Server | Roster scope | Search | Threads and receipts | File reservations | Wakes an idle session |
| ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ----------------------------- | ------------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------- |
| Swarmail | Per repository; worktrees share it | Full text (FTS5) | Threads; read, acknowledged and delivery receipts | Advisory, with a git guard | Claude Code and Cursor |
| [mcp_agent_mail_rust](https://github.com/Dicklesworthstone/mcp_agent_mail_rust/tree/21a25c2bcfd20c9b31bcb109c294d17411eb5ba1) | Per project path | Full text (Tantivy) | Threads; read, acknowledged and delivery receipts | Advisory, with a git guard | No; Claude Code hooks check the inbox during a turn |
| [mcp_agent_mail](https://github.com/Dicklesworthstone/mcp_agent_mail/tree/3fad5ec672869f81d2ca0a4c525dde004ac96f45) | Per project path | Full text (FTS5) | Threads; read and acknowledged | Advisory, with a git guard that is off by default | No; Claude Code, Codex and Factory hooks check the inbox during a turn |
| [agent-inbox](https://github.com/salimfadhley/agent-inbox/tree/a86647c2d61265e9a12fbc9ea0826a27712492cb) | One per hub | Substring, at most 25 results | Threads; read state | None | Claude Code (opt-in hook), opencode, omp |
| [agentbus](https://github.com/oznotes/AgentCommBus/tree/225a57c91180421bf2d698328a57f488f18bd820) | One per server | None | No threads; reading moves a cursor | None | No; an agent waits in `recv` for up to 600 s |
| [Project Relay](https://github.com/MuhammadFarhantahirvoltic/project-relay/tree/36d9a0765e4469f667c2e09fafe745166493fa86) | Per project | None | Replies; acknowledged per recipient | Advisory leases | No; an inbox read waits for up to 25 s |
An inbox read that waits for mail needs an agent already running. It does not
wake an idle session.
## Performance
Measured on 2026-10-04 at
[`67e187a`](https://github.com/bompus/swarmail/tree/67e187a6b4beb7319c6ab30466448742297e2a01).
These results come from a commit six commits before the 0.2.0 tag and have not been remeasured for 0.2.0 or any later release.
The measurements used one machine with one small workload (40 agents, 250 seed messages,
1,560 messages by the end). Each server started on empty storage. Startup is
hyperfine's mean of 20 runs; every other number is the median of three rounds,
with latency from Tinybench and requests per second from oha. [docs/benchmarks.md](docs/benchmarks.md) has
the method, a sixth server, each tool's raw output and a feature comparison
with seven other local agent-mail servers.
<a href="docs/assets/benchmark-light.png"><picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/benchmark-dark.png">
<img src="docs/assets/benchmark-light.png" alt="Bar charts sorted from best to worst for each metric. Send p50: Swarmail 0.46 ms, agentbus 2.3 ms, Project Relay 2.5 ms, Rust Agent Mail 39 ms, Python Agent Mail 70 ms. Search p50: Swarmail 0.65 ms, Python Agent Mail 12 ms, Rust Agent Mail 56 ms; agentbus and Project Relay have no search tool. Startup: Swarmail 38 ms, Project Relay 123 ms, agentbus 408 ms, Python Agent Mail 0.86 s, Rust Agent Mail 1.5 s. Idle memory: Swarmail 32 MiB, agentbus 81 MiB, Project Relay 121 MiB, Python Agent Mail 154 MiB, Rust Agent Mail 190 MiB." width="100%">
</picture></a>
Each chart ranks its metric from best to worst. Lower values are better in
all four charts. The tables below put servers in rows and units in headings.
Rust Agent Mail and Python Agent Mail are
`mcp_agent_mail_rust` and `mcp_agent_mail`.
### Latency
Median latency in milliseconds; lower is better. Rows are sorted by send
latency. Search has a different ranking. A dash means no search tool.
| Server | Send ↓ | Inbox ↓ | Search ↓ |
| :---------------- | -------: | -------: | -------: |
| **Swarmail** | **0.46** | **0.50** | **0.65** |
| agentbus | 2.3 | 1.1 | — |
| Project Relay | 2.5 | 1.4 | — |
| Rust Agent Mail | 39 | 12 | 56 |
| Python Agent Mail | 70 | 23 | 12 |
### Throughput
Requests per second with eight clients; higher is better. Rows are sorted by
send throughput. Search has a different ranking.
| Server | Send ↑ | Inbox ↑ | Search ↑ |
| :---------------- | --------: | --------: | --------: |
| **Swarmail** | **5,500** | **6,300** | **4,300** |
| agentbus | 876 | 1,600 | — |
| Project Relay | 482 | 1,100 | — |
| Rust Agent Mail | 50 | 416 | 90 |
| Python Agent Mail | 9.7 | 27 | 44 |
### Memory
Resident memory in MiB; lower is better. Rows are sorted by idle memory.
Peak memory has a different ranking.
| Server | Idle ↓ | Peak under load ↓ |
| :---------------- | -----: | ----------------: |
| **Swarmail** | **32** | **67** |
| agentbus | 81 | 96 |
| Project Relay | 121 | 296 |
| Python Agent Mail | 154 | 258 |
| Rust Agent Mail | 190 | 688 |
CPU usage, seed-loading times and the full ratios are in the
[detailed results](docs/benchmarks.md#summary).
### Startup
Mean startup time in milliseconds across 20 hyperfine runs; lower is better.
Rows are sorted by mean time. Each run starts the server, waits for its health
check, then stops it.
| Server | Mean ± SD ↓ | Min | Max |
| :---------------- | -------------: | -------: | -------: |
| **Swarmail** | **37.7 ± 0.7** | **37.1** | **39.3** |
| Project Relay | 122.9 ± 5.3 | 113.8 | 132.7 |
| agent-inbox* | 263.0 ± 11.4 | 252.0 | 297.9 |
| agentbus | 408.2 ± 15.6 | 385.1 | 443.3 |
| Python Agent Mail | 857.7 ± 38.7 | 803.5 | 925.7 |
| Rust Agent Mail | 1530.3 ± 14.9 | 1509.6 | 1562.8 |
The harness without a server takes 7.2 ± 0.6 ms (min 6.6, max 8.5).
Hyperfine's raw `Relative` column compares against that harness, not swarmail;
see the [startup output](docs/benchmarks.md#startup-hyperfine).
\* agent-inbox appears only in startup here. Its inbox pages hold 50 messages
instead of 20, its roster is global and its search matches substrings, so its
fetch, list and search do different work. The
[detailed comparison](docs/benchmarks.md) has its full column.
mcp_agent_mail commits each send to a Git archive before it returns, so these
numbers don't compare durability. Requests per second varied by up to 29%
between rounds of the same server.
## Development
Bun's formatter checks authored Markdown, HTML, JSON, JSONC, YAML, TOML and CSS as well
as JavaScript and TypeScript. The generated dependency lockfile stays
excluded. Package fields and dependency maps are sorted; script order is
preserved.
[Archived validator research](https://github.com/bompus/swarmail/blob/d55186b117c66bc522f4d2b4b041cd949eb0dc73/docs/research/validators-2026-10-06.md)
records the generated/runtime comparison, TypeBox compatibility findings and the
limits of the measurements for the tested mail contracts.
```bash
bun install
bun test
bun run check # format, lint and type check
```
`bun run check` checks formatting with Bun's `bun fmt` and type-checks with `bun check`, which need Bun 1.4.4 or later.
`bun scripts/build.ts --if-stale` rebuilds the binary only when a source file,
the dependency manifest or lockfile, the bytecode-order profile, the build
script, or the Bun version changed. Restart the unit
to load a new build.
The build passes `scripts/swarmail.order` to Bun's `--bytecode-order` (Bun 1.4.3 or later; earlier versions ignore it).
After a change to what the server runs at start-up or in its common calls, record the profile again with
`bun scripts/train-bytecode-order.ts` and commit the new file. A profile from older sources still applies, because Bun
matches functions by a hash of their syntax.
## Sponsoring
swarmail is built and maintained by one person. If it saves you time, you can
sponsor it monthly or once through
[GitHub Sponsors](https://github.com/sponsors/bompus), or leave a tip on
[Ko-fi](https://ko-fi.com/bompus).
## Licence
MIT. `CODE_OF_CONDUCT.md` is the Contributor Covenant under CC BY 4.0; see
`THIRD_PARTY_NOTICES.md`. To contribute, see [CONTRIBUTING.md](CONTRIBUTING.md).
TDQS
Scored across 23 tools
Most tools have clearly distinct resource/action targets, and descriptions explicitly cross-reference alternatives (e.g. whois vs list_agents, send_message vs reply_message, mark_message_read vs acknowledge_message). Minor overlap remains between fetch_inbox and fetch_session_inbox, and macro_start_session wrappers duplicate register_agent/ensure_project/file_reservation_paths, but these are well explained.
Names are consistently snake_case and mostly follow verb_noun patterns (send_message, fetch_inbox, retire_agent, release_file_reservations). Deviations like whois, health_check, and file_reservation_paths are noun-only or lack a verb, and retrieval verbs vary across fetch/get/list/search, but the conventions remain readable and predictable.
At 23 tools, the surface is heavy for a coordination server and sits at the borderline of the 16-25 range. Each tool is domain-relevant, but macro wrappers and separate per-agent/session inbox tools add breadth that could be consolidated.
The core lifecycle is well covered: send/reply/withdraw/search/read/acknowledge/summarize, agent register/retire/unretire/list/whois, and reserve/list/renew/release for files, plus health checks and project creation. Minor gaps exist, such as no list_projects, no direct get_message by ID, and no project update/delete, but agents can work around these.