Wire a fifth pull hook so peer messages also surface between todo items, not only at user prompts and subagent stops. While here, extend the manual `install-hook` CLI so it patches the full plugin hook set (SessionStart/UserPromptSubmit/SubagentStop/TaskCompleted/ SessionEnd) instead of only UserPromptSubmit, mirroring what the plugin's hooks.json registers. Mailbox name is auto-derived from stdin, so --name is no longer required. Also corrects stale docs that claimed SessionStart auto-bootstraps the watcher — push delivery has been opt-in since the mailbox-collaborate skill landed.
266 lines
10 KiB
Markdown
266 lines
10 KiB
Markdown
# ClaudeMailbox
|
|
|
|
A standalone MCP mail server that lets parallel Claude sessions coordinate with each other. Messages are queued in a tiny SQLite database via a local HTTP daemon. Any Claude session — Claude Code, ClaudeDo worktree, plain MCP client — can send to a peer's inbox, check for pending messages, and discover other active mailboxes.
|
|
|
|
Not a substitute for `run_in_background: true` (which handles single-session responsiveness). This handles **session-to-session** coordination.
|
|
|
|
---
|
|
|
|
## Getting started
|
|
|
|
Pick one path. Most users want path A.
|
|
|
|
### A. Claude Code plugin (recommended — three prompts)
|
|
|
|
Inside Claude Code:
|
|
|
|
```
|
|
/plugin marketplace add https://git.kuns.dev/releases/ClaudeMailbox.git
|
|
/plugin install claude-mailbox@claude-mailbox
|
|
/claude-mailbox:mailbox-doctor
|
|
```
|
|
|
|
The doctor command does the rest:
|
|
|
|
1. installs the daemon binary via `npm install -g @kuns/claude-mailbox` if missing (asks first)
|
|
2. registers the daemon for autostart and starts it
|
|
3. optionally lets you pick a base prefix (e.g. `backend`, `frontend`); without one, mailbox names are anonymous (`claude-a8b3c1d2`)
|
|
4. runs a self → self smoke test
|
|
|
|
After that, every Claude Code session automatically:
|
|
|
|
- gets a **unique mailbox identity** derived from its session UUID (so two parallel sessions never collide),
|
|
- announces that identity and the **list of currently active peers** at session start,
|
|
- pulls unread mailbox messages into context before every prompt.
|
|
|
|
You can then say things like:
|
|
|
|
> "I started a second session, coordinate with it on the refactor."
|
|
|
|
Claude already has the peer's mailbox name in context from the SessionStart announcement, so it calls `mcp__mailbox__send(from="<my-name>", to="<peer>", body="...")` directly.
|
|
|
|
See [`plugin/README.md`](./plugin/README.md) for the full walkthrough, including the `mailbox-status` and `mailbox-update` slash commands.
|
|
|
|
### B. Manual install (no Claude Code plugin)
|
|
|
|
If you're using a different MCP client, scripts, or you don't want the plugin:
|
|
|
|
```sh
|
|
# one-time per machine: point the @kuns scope at the public Gitea npm registry
|
|
npm config set @kuns:registry=https://git.kuns.dev/api/packages/releases/npm/
|
|
|
|
# install + autostart
|
|
npm install -g @kuns/claude-mailbox
|
|
claude-mailbox install-autostart
|
|
```
|
|
|
|
Or the bootstrap one-liner:
|
|
|
|
```powershell
|
|
# Windows
|
|
irm https://git.kuns.dev/releases/ClaudeMailbox/raw/branch/main/install.ps1 | iex
|
|
```
|
|
|
|
```sh
|
|
# macOS / Linux
|
|
curl -fsSL https://git.kuns.dev/releases/ClaudeMailbox/raw/branch/main/install.sh | sh
|
|
```
|
|
|
|
Then drop this into your project's `.mcp.json`:
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"mailbox": {
|
|
"type": "http",
|
|
"url": "http://127.0.0.1:37849/mcp"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Optionally add a static identity (so your client doesn't need to pass `from` / `name` on every call):
|
|
|
|
```json
|
|
"headers": { "X-Mailbox": "backend" }
|
|
```
|
|
|
|
---
|
|
|
|
## How identity works
|
|
|
|
Every Claude Code session gets a unique mailbox name automatically derived as `<project>-<8-hex-of-session-id>`:
|
|
|
|
| Setup | Resulting mailbox name |
|
|
|---|---|
|
|
| Inside a git repo | `<repo-basename>-<8-hex>` (e.g. `claude-mailbox-a3f91b2c`) |
|
|
| Outside a git repo | `<cwd-basename>-<8-hex>` |
|
|
| No cwd available (rare) | `claude-<8-hex>` |
|
|
| Manual `.mcp.json` with `X-Mailbox: backend` header (no plugin) | `backend` (legacy mode) |
|
|
|
|
Project names are sanitized (lowercased, non-alphanumerics → dashes, capped at 40 chars). The plugin's `SessionStart` hook prints the session's identity and the list of peers active in the last hour into the conversation context, so Claude knows who it is and who's around without needing to call any tools first.
|
|
|
|
### Renaming at runtime
|
|
|
|
Claude can refine its own mailbox name during the session — useful when a session focuses on a specific area (e.g. only frontend work):
|
|
|
|
```
|
|
mcp__mailbox__rename(current_name="claude-mailbox-a3f91b2c", new_name="claude-mailbox-frontend-a3f91b2c")
|
|
```
|
|
|
|
Pending messages are transferred to the new name in a single transaction. The old name is removed — peers using it must re-discover via `list_mailboxes`. The endpoint returns `409` if the target name is already in use.
|
|
|
|
---
|
|
|
|
## Autostart
|
|
|
|
```sh
|
|
claude-mailbox install-autostart # per-user, no admin
|
|
claude-mailbox install-autostart --service # Windows only: Windows Service (admin)
|
|
claude-mailbox status # Running | Stopped | NotInstalled
|
|
claude-mailbox uninstall-autostart [--purge]
|
|
```
|
|
|
|
| Platform | Default mechanism | `--service` mechanism |
|
|
|---|---|---|
|
|
| Windows | Scheduled Task at logon (no admin); falls back to HKCU Run-key if Group Policy blocks schtasks | Windows Service (admin, via `node-windows`) |
|
|
| macOS | launchd LaunchAgent in `~/Library/LaunchAgents/` | n/a |
|
|
| Linux | systemd `--user` unit in `~/.config/systemd/user/` | n/a |
|
|
|
|
---
|
|
|
|
## MCP tools
|
|
|
|
| Tool | Required args | Purpose |
|
|
|---|---|---|
|
|
| `mcp__mailbox__send` | `to`, `body`, `from` | Send a message. `from` falls back to X-Mailbox header. |
|
|
| `mcp__mailbox__check_inbox` | `name` | Pull all pending messages and mark delivered. Falls back to header. |
|
|
| `mcp__mailbox__peek_inbox` | `name` | Non-consuming `{ pending, oldestAt }`. Falls back to header. |
|
|
| `mcp__mailbox__list_mailboxes` | `name` | Discover known mailboxes + `pendingForYou`. Falls back to header. |
|
|
|
|
The plugin's SessionStart announcement tells Claude exactly which name to pass for the current session, so the args are filled in automatically.
|
|
|
|
### Suggested CLAUDE.md snippet for poll discipline
|
|
|
|
```
|
|
When coordinating with a peer session, call mcp__mailbox__peek_inbox
|
|
after each subagent completes. If pending > 0, call mcp__mailbox__check_inbox
|
|
and treat the messages as input with priority over the current plan.
|
|
```
|
|
|
|
---
|
|
|
|
## Push delivery (watch)
|
|
|
|
The `watch --block` subcommand turns mail delivery from pull (poll between turns) into push (the receiver reacts as soon as a peer sends). It's a long-poll that exits the moment one message arrives.
|
|
|
|
```
|
|
claude-mailbox watch --block --name <mailbox> [--timeout 25] [--url <daemon>]
|
|
```
|
|
|
|
Intended use: a Claude Code background bash task. The plugin's `SessionStart` hook now tells Claude to start one on its first turn, so peers can `mcp__mailbox__send` to it and Claude reacts mid-session via `BashOutput` — no user prompt needed. After every exit Claude relaunches the watcher in the background.
|
|
|
|
| Exit code | Meaning |
|
|
|---|---|
|
|
| `0` | One message delivered (or mailbox renamed — stdout disambiguates) |
|
|
| `1` | Generic error (e.g. missing `--name`) |
|
|
| `2` | Daemon unreachable |
|
|
| `3` | Timeout reached with no message |
|
|
|
|
The CLI consumes exactly one message per cycle (single-delivery, FIFO winner across concurrent watchers on the same mailbox). Backlog drains one message per reconnect (~100 ms turnaround).
|
|
|
|
Cross-process semantics:
|
|
- **Concurrent watchers on the same mailbox:** the first to register wins each individual message; others continue waiting.
|
|
- **Rename mid-watch:** the open `watch` exits 0 with a `Mailbox renamed to '<new>'` notice; relaunch with the new `--name`.
|
|
- **Daemon restart:** all watchers see exit 2; back off and retry.
|
|
- **Session end:** Claude Code reaps background bash on exit; the `fetch` aborts and the daemon-side waiter is cleaned up.
|
|
|
|
**When push helps:** during active turns where the receiver is busy with tool calls — `BashOutput` notifications surface between tool calls, so peer messages arrive mid-turn. **When push degrades to pull:** when the receiver is idle between turns, BashOutput is buffered until the next user prompt, at which point the existing `UserPromptSubmit` poll hook delivers the same message. The two channels coexist.
|
|
|
|
---
|
|
|
|
## CLI
|
|
|
|
Any external process — scripts, UIs, manual debugging — can talk to a running daemon directly:
|
|
|
|
```
|
|
claude-mailbox send --from <mailbox> --to <mailbox> --body <text>
|
|
claude-mailbox peek --name <mailbox>
|
|
claude-mailbox check --name <mailbox> [--hook]
|
|
claude-mailbox watch --block --name <mailbox> [--timeout 25]
|
|
claude-mailbox list
|
|
claude-mailbox status
|
|
claude-mailbox session-announce # hook helper, reads stdin JSON
|
|
claude-mailbox install-hook [--user|--project] [--url <url>]
|
|
claude-mailbox uninstall-hook [--user|--project]
|
|
```
|
|
|
|
All subcommands accept `--url <url>` to target a non-default daemon address.
|
|
|
|
---
|
|
|
|
## REST surface
|
|
|
|
| Method | Path | `X-Mailbox` required | Purpose |
|
|
|---|---|---|---|
|
|
| `GET` | `/health` | no | `{ status, version, dbPath }` |
|
|
| `POST` | `/v1/send` | yes (sender) | body: `{ to, body }` |
|
|
| `GET` | `/v1/peek?name=<mailbox>` | no | read-only status |
|
|
| `POST` | `/v1/check-inbox?name=<mailbox>` | yes (must match `name`) | consume inbox |
|
|
| `GET` | `/v1/watch?name=<mailbox>&timeout=<sec>` | yes (must match `name`) | long-poll one message: `200` + body / `204` timeout / `409 { reason: "renamed", to }` |
|
|
| `GET` | `/v1/list` | optional (presence registers caller) | list all mailboxes |
|
|
|
|
---
|
|
|
|
## Config precedence
|
|
|
|
```
|
|
CLI flag > mailbox.json > built-in defaults
|
|
```
|
|
|
|
`mailbox.json` is searched at `~/.claude-mailbox/mailbox.json` (per-user), and on Windows additionally at `%ProgramData%\ClaudeMailbox\mailbox.json` (machine-wide, written by `--service` install). Override with `--config <path>`.
|
|
|
|
Defaults: port `37849`, bind `127.0.0.1`, database at `~/.claude-mailbox/mailbox.db`.
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
One long-running daemon binds HTTP on loopback, hosts the MCP server at `/mcp` and a small REST API at `/v1/*`, and persists state in a single SQLite file.
|
|
|
|
```
|
|
session-A session-B external sender
|
|
mailbox: claude-a8b3c1d2 mailbox: claude-d4e5f6a7 (CLI / UI / script)
|
|
| | |
|
|
| HTTP | |
|
|
+--------------+-----------------+--------------------------+
|
|
v
|
|
claude-mailbox serve (Fastify)
|
|
/mcp MCP tools
|
|
/v1/* REST for non-MCP senders
|
|
/health
|
|
v
|
|
~/.claude-mailbox/mailbox.db (SQLite WAL)
|
|
```
|
|
|
|
---
|
|
|
|
## Development
|
|
|
|
```sh
|
|
cd node
|
|
npm install
|
|
npm run build
|
|
npm test
|
|
```
|
|
|
|
The test suite covers end-to-end coordination, concurrent `check_inbox` race safety, schema idempotency, hook stdin parsing, session-id derivation, and settings-file patching.
|
|
|
|
---
|
|
|
|
## Scope
|
|
|
|
- Loopback bind only (v1). Cross-machine coordination is a future extension — swap the middleware for token auth and change the bind address.
|
|
- No auth on loopback. Local filesystem permissions are the trust boundary.
|
|
- No message expiry. Delivered messages remain as an audit log.
|