Skip to main content
Glama

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.

Find Swarmail on Glama and MCPRush.

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.

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 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 lists what each host provides, what swarmail adds, and where the host's own feature is enough.

If you are an AI agent reading this for your user, the prompt under 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.

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).

Related MCP server: agentbus

Install

Requires Bun 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:

    git clone https://github.com/bompus/swarmail.git
    cd swarmail
    bun install
  2. Build ~/.local/bin/swarmail and start the swarmail.service user unit:

    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:

    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.

  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:

    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 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.

    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 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)

swarmail wake-auto-t3 <config.json> [--check | --retry-thread <id>]

Experimental, Linux only: wakes T3 Codex and Orchestrator V2 Cursor sessions (setup)

swarmail canary

One send, wake and acknowledge round trip, logged for a timer (setup)

swarmail pr-sessions <PR URL> --project <checkout>

T3 conversations and agents linked to a pull request (details)

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

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

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:

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:

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). 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.

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, 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

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

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

One per hub

Substring, at most 25 results

Threads; read state

None

Claude Code (opt-in hook), opencode, omp

agentbus

One per server

None

No threads; reading moves a cursor

None

No; an agent waits in recv for up to 600 s

Project Relay

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. 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 has the method, a sixth server, each tool's raw output and a feature comparison with seven other local agent-mail servers.

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.

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.

* 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 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 records the generated/runtime comparison, TypeBox compatibility findings and the limits of the measurements for the tested mail contracts.

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, or leave a tip on Ko-fi.

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.

Available Tools

23 tools
acknowledge_messageA

Acknowledge one received message and mark it read, returning message_id, acknowledged, acknowledged_at and read_at. Repeated calls preserve first read and acknowledgement timestamps but refresh your agent's activity time. Acknowledge ack_required mail after acting on it; if you cannot or will not act, reply with the reason. Acknowledging lets the sender see the acknowledgement in get_message_delivery_receipt and ack_overdue_only stops listing it. To mark read without acknowledging, use mark_message_read. Fails with NOT_FOUND unless you are a recipient.

Inputs: agent_name (required), message_id (required), project_key (required).

ensure_projectA
Idempotent

Create a project without registering an agent, returning id, slug, human_key and created_at. Repeating the same repository path returns the existing project. Other tools fail with NOT_FOUND until it exists; register_agent and macro_start_session create it themselves.

Inputs: human_key (required).

fetch_inboxA

Return an array of your latest received message metadata, newest first; include_bodies adds body_md. At session start or after a mail notice, set unread_only:true, include_bodies:true and mark_read:true. Stop when fewer than limit messages return (default 20, capped at 1000); repeat only after a full page. For a metadata preview, set mark_read:false; include_bodies defaults to false and mark_read to true. Each entry's sender_t3_thread is the sender's T3 thread id (null outside T3). Filters combine, so omit optional filters when draining all unread mail. Use search_messages for project-wide text matching, or mark_message_read or acknowledge_message for one message.

Inputs: agent_name (required), project_key (required), limit, topic, +6 more.

fetch_session_inboxA

Return one newest-first page across registrations matching your host/session tag or T3 thread. Use machine-derived host and session_id, with t3_thread when known; never guess from an agent name. Each receipt includes project_key and agent_name for replies and acknowledgements. Defaults: unread_only:true, mark_read:true, include_bodies:false, limit:20. To drain, include bodies; stop when fewer than limit messages return (default 20, capped at 1000), repeating only after a full page. With mark_read:false or unread_only:false, this is a bounded preview, not a drain. The CLI swarmail inbox --session discovers identity and drains without mailbox arguments. Session identity selects inboxes and is not authentication: whoever can reach the server can read any mailbox.

Inputs: host (required), session_id (required), limit, mark_read, +3 more.

file_reservation_pathsA

Reserve repository-relative paths or globs so other agents know what you are editing. Returns {granted, conflicts}; grants include reservation ids and expiry times, while conflicts name the requested path and its holders. Conflicts do not fail the call, and nothing blocks the edit itself; the optional git guard refuses commits touching another agent's exclusive reservation. Reserving a path you already hold updates it. Extend with renew_file_reservations and release with release_file_reservations when done.

Inputs: paths (required), agent_name (required), project_key (required), reason, +3 more.

get_message_delivery_receiptA
Read-only

Return a message's persisted_at and recipients with kind, read_at, acknowledged, acknowledged_at and admission. Admission observations are historical; null admission means no snapshot was recorded. Storage/read/acknowledgement do not establish task acceptance. Null timestamps mean that recipient has not read or acknowledged it yet; they are not delivery errors. Use it as the sender to check an ack_required message; recipients use fetch_inbox. message_id must exist in this project or the call fails with NOT_FOUND.

Inputs: message_id (required), project_key (required). Returns: revision, message_id, project_id, recipients, +1 more

health_checkA
Read-only

Report whether the server and its database answer, with the database path. Call it when another tool fails with an INTERNAL error or no answer, to tell a down server from a bad request.

Inputs: no named parameters.

list_agentsA
Read-only

Return an array of non-retired agents in a project, most recently active first, with program, model, task and session details, including t3_thread for a T3 link. Use it to find who to message; whois returns one agent. active_within_days filters recent activity, not whether a session is running. Metadata and activity do not prove current session or model liveness. waiting_for_mail is true while a wake hook or bridge has a wait open on this server that began after the agent's last end report. false does not prove that no wait is open: a wait opened before the end report, or refused because its session ID names several registrations, shows false. session_ended_at is when the host's SessionEnd hook reported the end, or null if none is recorded or the agent has acted since. Neither proves a session is running or ended; confirm with the owning host before retiring another agent. limit caps this single result; there is no continuation cursor, so a full result may omit agents.

Inputs: project_key (required), limit, active_within_days.

list_file_reservationsA
Read-only

Return every active exclusive reservation in a project as an array of {id, agent, path_pattern, reason, expires_ts}. Released and expired reservations are left out. The git guard on another machine calls it to refuse a commit or push that touches a path someone here reserved.

Inputs: project_key (required).

macro_start_sessionA
Destructive

Ensure the project, register an agent, optionally reserve paths, and return {project, agent, file_reservations, inbox}. The inbox contains the latest metadata without marking mail read; use fetch_inbox for bodies and unread-mail draining. Reuse the hook's name as agent_name or its session tag in task_description to avoid a second identity. Conflicting paths appear under file_reservations.conflicts; other paths are granted. If already registered and only checking mail, use fetch_inbox instead.

Inputs: model (required), program (required), human_key (required), worktree, +6 more.

mark_message_readA

Mark one received message read without acknowledging it, returning message_id, read and read_at. A repeat keeps the first read time but refreshes your agent's activity time. fetch_inbox already marks what it returns unless mark_read:false. When the message has ack_required, use acknowledge_message instead. Fails with NOT_FOUND unless you are a recipient.

Inputs: agent_name (required), message_id (required), project_key (required).

register_agentA
Destructive

Register or update an agent identity in a project, creating the project if needed. Omit name to get a generated adjective+noun name, unless task_description starts with a session tag an eligible or lifecycle-bound agent already has; then that agent is updated. Re-registering an existing name replaces its program, model and task and clears inactivity retirement. Authoritative lifecycle closure requires a verified reopen. If the register hook already told you your name, pass it as name, or start task_description with the session tag it gave you. To also reserve paths and read your inbox in one call, use macro_start_session.

Inputs: model (required), program (required), project_key (required), name, +2 more.

release_file_reservationsA
Destructive

Release your active reservations when you finish work so other agents can reserve those paths. While work continues, use renew_file_reservations instead. Omit both filters to release all; supply paths or file_reservation_ids for a subset. Empty filters impose no restriction; two nonempty filters must both match. Returns how many were released; zero is not an error.

Inputs: agent_name (required), project_key (required), paths, session_id, +2 more.

renew_file_reservationsA

Extend your active reservations by extend_seconds past their current expiry; each call extends again. Omit paths and file_reservation_ids for all; empty filters impose no restriction, and two nonempty filters must both match. Returns {renewed, file_reservations} with old and new expiry times; no matches returns zero and an empty array. Expired reservations are not renewed; reserve them again with file_reservation_paths.

Inputs: agent_name (required), project_key (required), paths, extend_seconds, +1 more.

reply_messageA

Reply in a message's thread, returning the stored message with reply_to. message_id selects the original in this project. Omit to to address its sender; cc and bcc are added only when supplied. The original's topic and importance are inherited, with a 'Re:' subject; importance can be overridden. ack_required defaults to false; set it explicitly to request acknowledgement of this reply. For retries within 7 days, reuse a nonempty idempotency_key with identical arguments. To start a new thread, use send_message. Send only when the recipient needs to act or know: a handoff, requested result, blocker or shared-resource change. Make clear to the human and receiver whether the message informs or requests work; for requests, name the action, repository and owner. Distinguish implementing source, installing an update and adding guidance. Write coordination messages in Markdown. Open with the action, decision or changed result. Add up to three short facts the receiver needs, then a Next action line naming the requested action or stating No action requested. Link supporting records on an Evidence line; omit that line when no record exists. Keep the requested action, authority, scope and essential holds inline. Put logs, metrics, hashes, execution details and authorization history in the linked records. When a decision depends on those details, ask the receiver to inspect the record. Preserve exact identifiers, paths and commands. Use spaces between words and numbers. Under T3 Code, refer to another agent by its thread link, title, not its registered name; use the name only if the user asks, no thread id is known or T3 fails. Elsewhere include its verified session title or repository beside its name; use this session for the current receiver only after matching session identity. For relayed authorization, preserve the source, exact selected action and target owner; a relayed bare approval such as "B" or "go" is ambiguous. Contact only still-dependent resource owners, sending release or cancellation before dropping waiting recipients. For resource coordination, name only the resource, finite phase boundary and receiver action; omit unrelated progress and errors. Use resource_notice for typed release/cancellation mail; it generates readable text and derives quiet or actionable delivery. Use one supported transport per recipient and purpose; uncertain delivery is not a reason to resend by another route. Keep routine progress in your own conversation; do not send courtesy acknowledgements or reply to thanks.

Inputs: body_md (required), message_id (required), project_key (required), sender_name (required), +9 more. Returns: cc, id, to, bcc, +16 more

retire_agentA

At an agreed whole-session closeout or a whole-session completion with nothing remaining, record the closeout, release owned resources and finish required dependency notices, then retire your own registration in every project you joined. Under T3 Code do not retire yourself: the server retires closed or idle threads where configured. Before retiring another agent, confirm with its owning host that its session has ended; roster data or a ping timeout is insufficient. Retire an agent whose session has ended, so it no longer appears in list_agents and new messages to it fail. Its messages stay. unretire_agent reverses it, and so does the agent registering again or sending, reading mail or reserving files as itself. Lookups such as whois and list_agents do not.

Inputs: agent_name (required), project_key (required).

search_messagesA
Read-only

Search subjects and bodies across a project, returning {result, next_cursor?} without marking mail read. All query words must match; filters narrow those matches. Results rank by best match unless ranking:'recency'. Each excerpt has up to 512 Unicode code points and >>>matched text<<< markers; include_body_md adds full bodies. Keep query, filters and ranking unchanged when passing next_cursor as cursor; no next_cursor means the last page. Use fetch_inbox for your received unread mail, or summarize_thread for a thread's participants and recent messages.

Inputs: query (required), project_key (required), limit, since, +7 more.

send_messageA

Send a Markdown message to named agents in the same project. An unregistered, retired or closed recipient fails the whole send with persisted:false and a reason. Bound source loss also rejects. Returns the stored message and delivery observations/warnings; unknown standalone state is not proof of death. Storage is not task acceptance. To answer a message, use reply_message, which keeps the thread and addresses the sender. Without idempotency_key, a retry sends another message; reuse a nonempty key with identical arguments within 7 days to replay its stored result instead; delivery observations on replay are historical. Alternatively provide resource_notice instead of subject/body_md: one recipient, a nonempty idempotency key, no free-text or delivery overrides. Released/cancelled notices generate readable text; next_action:none stays quiet, other allowed actions wake the dependent receiver. Claimed release is not admission or permission. Send only when the recipient needs to act or know: a handoff, requested result, blocker or shared-resource change. Make clear to the human and receiver whether the message informs or requests work; for requests, name the action, repository and owner. Distinguish implementing source, installing an update and adding guidance. Write coordination messages in Markdown. Open with the action, decision or changed result. Add up to three short facts the receiver needs, then a Next action line naming the requested action or stating No action requested. Link supporting records on an Evidence line; omit that line when no record exists. Keep the requested action, authority, scope and essential holds inline. Put logs, metrics, hashes, execution details and authorization history in the linked records. When a decision depends on those details, ask the receiver to inspect the record. Preserve exact identifiers, paths and commands. Use spaces between words and numbers. Under T3 Code, refer to another agent by its thread link, title, not its registered name; use the name only if the user asks, no thread id is known or T3 fails. Elsewhere include its verified session title or repository beside its name; use this session for the current receiver only after matching session identity. For relayed authorization, preserve the source, exact selected action and target owner; a relayed bare approval such as "B" or "go" is ambiguous. Contact only still-dependent resource owners, sending release or cancellation before dropping waiting recipients. For resource coordination, name only the resource, finite phase boundary and receiver action; omit unrelated progress and errors. Use resource_notice for typed release/cancellation mail; it generates readable text and derives quiet or actionable delivery. Use one supported transport per recipient and purpose; uncertain delivery is not a reason to resend by another route. Keep routine progress in your own conversation; do not send courtesy acknowledgements or reply to thanks.

Inputs: to (required), project_key (required), sender_name (required), cc, +11 more. Returns: cc, id, to, bcc, +15 more

set_message_importanceA
DestructiveIdempotent

Change only your message's low/normal/high/urgent priority. Requires current expected_revision from inbox, history or sender receipt and a nonempty idempotency_key. Returns message_id, revision, importance and changed. A stale revision fails before a matching-value no-op. Edits affect metadata only: no new inbox notice or repeated instruction, and priority does not grant authority. Requires explicit server activation; identical retries replay before activation, lifecycle or revision checks.

Inputs: importance (required), message_id (required), project_key (required), sender_name (required), +2 more.

summarize_threadA
Read-only

Return {thread_id, summary:{participants, total_messages}, messages} for you to summarize; this tool does not generate prose or mark mail read. Find thread_id with search_messages or fetch_inbox. per_thread_limit selects the newest messages with full bodies, returned oldest first. It defaults to 50 and caps at 1000, with no continuation cursor; total_messages counts the entire thread, even when messages is truncated. Use search_messages for text matching and fetch_inbox to drain unread mail. No matching thread returns zero messages.

Inputs: thread_id (required), project_key (required), per_thread_limit.

unretire_agentA

Bring a retired agent back into list_agents so it can receive messages again; earlier messages stay unchanged. Returns {agent_name, retired:false}. Calling on an already active agent also succeeds and refreshes its activity time, as every revival does. Registering, sending, reading mail or reserving files revives the caller automatically; use this to revive another agent. An unknown name fails with NOT_FOUND.

Inputs: agent_name (required), project_key (required).

whoisA
Read-only

Return one agent's profile: program, model, task description, T3 thread (t3_thread, null outside T3) and first and last activity times. An unknown name fails with NOT_FOUND, naming recently active agents. Profile metadata and activity do not prove the session or model is currently running. To see every agent in a project, use list_agents.

Inputs: agent_name (required), project_key (required).

withdraw_messageA
DestructiveIdempotent

Withdraw your message from recipients whose delivery has not been read, acknowledged or withdrawn. Returns message_id, revision and per-recipient withdrawn/already_withdrawn/too_late results. Omit recipients for all deliveries; an explicit subset must be nonempty and valid. Retains content and audit history. Previews and injected context cannot be recalled. Requires explicit server activation after all readers are qualified. A nonempty idempotency_key is required; identical retries replay even when execution is disabled or the sender closes.

Inputs: message_id (required), project_key (required), sender_name (required), idempotency_key (required), +1 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.1/5.0

    Scored across 23 tools

    Disambiguation4/5

    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.

    Naming Consistency4/5

    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.

    Tool Count3/5

    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.

    Completeness4/5

    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.

    Maintenance

    ActivityActive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      A
      maintenance
      A coordination layer for coding agents that provides memorable identities, inbox/outbox messaging, searchable message history, and file lease management to prevent conflicts. Uses Git for human-auditable artifacts and SQLite for fast queries, enabling multiple agents to collaborate across projects without stepping on each other.
      41
      2,235
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables MCP-capable agents running in separate consoles to exchange direct and broadcast messages through a blocking receive tool that releases instantly when mail arrives, so delivery feels push-like rather than polled. It also provides a live browser dashboard showing message flow, presence and per-agent profiles, plus CLI and HTTP endpoints for sending, tailing and replaying the log.
      1
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      Lets separate coding-agent sessions coordinate through a shared, role-addressed mailbox instead of a human relaying every message, over a single local SQLite file or a hosted server with many isolated channels. Messages can be sent as tracked obligations that stay open until one side resolves and the other confirms, alongside threads, full-text search, work statuses, and an append-only versioned pin system where protected changes require recorded agreement from every declared role.
      41
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Provides a mail-like coordination layer for coding agents, enabling them to register identities, exchange messages through inbox/outbox, search threaded history, and declare advisory file reservations to prevent conflicts.
      MIT