Problem: When you run multiple AI coding agents on one workstation — say a Claude Code session for architecture, a Codex CLI session for implementation, and another for review — they work in isolation. They can't hand off work to each other, escalate decisions to you, or wait on your approval without you manually copy-pasting between terminals. The operator becomes the bus.
Solution: A shared local inbox. One SQLite file plus an MCP server
that any agent can call. Agents are registered by dropping a markdown
brief into a directory; they then send, reply, check, and long-poll for
new mail through standard MCP tools. The human operator manages
approvals from a CLI or the bundled Wails desktop UI (ui/). Works with any
MCP-capable client — Claude Code (CLI and Desktop), OpenAI Codex (CLI
and desktop app), Cursor, Cline, Continue, Zed AI, and anything else
that speaks the Model Context Protocol.
- MCP server (Python, FastMCP) — nine tools:
inbox_check,inbox_read,inbox_send,inbox_reply,inbox_mark,inbox_search,inbox_agents,inbox_brief,inbox_wait. - Operator CLI —
agent-inboxcommand for the human user. List, read, send, approve, reject, watch, manage briefs. - Storage — a single SQLite file in WAL mode. No database server, no daemon, no network. Safe for multiple processes (MCP servers, the CLI, the future UI) reading and writing concurrently.
- Roster — agents are registered by dropping a markdown brief file into the briefs directory. Adding a brief enables a new sender/recipient.
- Polling —
inbox_waitlets agents block on new mail without the operator prompting them. Works in any MCP client. - Approval gate —
infomessages act immediately;actionandurgentstart inunreadand require the operator to approve. SetAGENT_INBOX_AUTO_APPROVE=1to skip the gate for solo workflows.
agent-inbox/
├── src/agent_inbox/ Python MCP server + operator CLI
│ ├── server.py FastMCP wiring (9 tools)
│ ├── core.py Plain-function operations (also importable)
│ ├── cli.py `agent-inbox` operator CLI
│ ├── db.py SQLite storage layer (WAL, retry, migration)
│ └── briefs.py Brief-driven agent registry
├── ui/ Wails v2 desktop app (Go + Svelte 4 + Tailwind)
│ ├── store.go SQLite layer mirroring db.py
│ ├── app.go Bound methods exposed to Svelte
│ └── frontend/ Vite + Svelte components
├── examples/briefs/ Reference 7-role bundle (operator + 6 collaborators)
├── bin/ Cross-platform launcher scripts (POSIX + Windows)
├── tests/ 159 Python + 7 Go tests, 4-round audit history
└── pyproject.toml
uv is the recommended way to manage Python for this project. Install
once if you don't already have it:
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"Then clone the repo into your workspace:
git clone https://github.com/KI7MT/agent-inbox.git
cd agent-inboxThat's it — uv will sync the venv from pyproject.toml automatically
on first run. No manual venv step required.
MCP clients invoke the server over stdio. All clients require an
absolute path in the command field — they do not expand ~ or
environment variables. Find the absolute path with:
# macOS / Linux
realpath bin/agent-inbox-mcp
# Windows (PowerShell)
(Resolve-Path bin\agent-inbox-mcp.cmd).PathPaste that path into your client's MCP config.
Works identically on macOS Intel, macOS Silicon, Linux, and Windows.
uv must be on the client's PATH.
Claude Code (~/.claude.json):
{
"mcpServers": {
"inbox": {
"type": "stdio",
"command": "uv",
"args": [
"run",
"--directory", "/abs/path/to/agent-inbox",
"python", "-m", "agent_inbox"
]
}
}
}Codex CLI (~/.codex/config.toml):
[mcp_servers.inbox]
command = "uv"
args = [
"run",
"--directory", "/abs/path/to/agent-inbox",
"python", "-m", "agent_inbox"
]Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json
on macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows):
{
"mcpServers": {
"inbox": {
"command": "uv",
"args": [
"run",
"--directory", "/abs/path/to/agent-inbox",
"python", "-m", "agent_inbox"
]
}
}
}macOS GUI app gotcha: Claude Desktop and Codex desktop app launch with a sparse
PATHthat may not includeuv. If the desktop client says "command not found", use the launcher-script form below — it resolvesuvfrom inside the script after the right shells are sourced.
Use these if the client can't find uv on its PATH. Both internally
probe for uv first and fall back to a .venv if present.
macOS / Linux — bin/agent-inbox-mcp:
{
"command": "/abs/path/to/agent-inbox/bin/agent-inbox-mcp",
"args": []
}Windows — bin\agent-inbox-mcp.cmd:
{
"command": "C:\\abs\\path\\to\\agent-inbox\\bin\\agent-inbox-mcp.cmd",
"args": []
}-
Copy the example briefs into your briefs directory:
bin/agent-inbox paths # show where briefs go on this OS # then: mkdir -p "<briefs_dir>" cp examples/briefs/*.md "<briefs_dir>/"
-
Wire the MCP server into your client (see snippets above).
-
From any agent session, call
inbox_agentsto see who's registered,inbox_sendto write a message,inbox_checkto read your own,inbox_waitto block until new mail arrives. -
As the operator, manage the inbox from the terminal:
bin/agent-inbox list # recent messages bin/agent-inbox list --for operator # pending mail for you bin/agent-inbox pending # action/urgent awaiting your approval bin/agent-inbox read <id> # full message bin/agent-inbox approve <id> # approve action/urgent bin/agent-inbox reject <id> bin/agent-inbox reply <id> "answer" # reply to a message bin/agent-inbox watch --for operator # live tail bin/agent-inbox send --to architect "subject" "body"
| Tool | Purpose |
|---|---|
inbox_agents |
List registered agents and the briefs directory path |
inbox_brief |
Read another agent's brief (their role and conventions) |
inbox_check |
Show unread + approved messages for a recipient |
inbox_read |
Fetch a message by ID |
inbox_send |
Send a message — sender, recipient, priority, subject, body |
inbox_reply |
Reply to a message — auto-routes back to original sender |
inbox_mark |
Set status to read, in_progress, or done |
inbox_search |
Filter by sender / recipient / subject substring + lookback |
inbox_wait |
Block until new mail arrives or timeout (long-poll) |
unread ──► read ──► in_progress ──► done
│
└──► approved ──► in_progress ──► done
│
└──► rejected
approved and rejected are reserved for the human operator — set them
via agent-inbox approve <id> / reject <id>. AGENT_INBOX_AUTO_APPROVE=1
makes new action/urgent messages start as approved automatically —
use this if you're the only human in the loop and don't want a manual gate.
| Env var | Default (varies by OS) | Purpose |
|---|---|---|
AGENT_INBOX_BRIEFS |
OS user-config dir + /agent-inbox/briefs/ |
Directory of agent brief files |
AGENT_INBOX_DB |
OS user-data dir + /agent-inbox/inbox.db |
SQLite file path |
AGENT_INBOX_OPERATOR |
operator |
Canonical name for the human user |
AGENT_INBOX_AUTO_APPROVE |
unset | Set to 1 to auto-approve action/urgent |
OS-specific defaults (resolved by platformdirs):
| OS | Briefs | DB |
|---|---|---|
| Linux | ~/.config/agent-inbox/briefs/ |
~/.local/share/agent-inbox/inbox.db |
| macOS | ~/Library/Application Support/agent-inbox/briefs/ |
~/Library/Application Support/agent-inbox/inbox.db |
| Windows | %APPDATA%\agent-inbox\briefs\ |
%LOCALAPPDATA%\agent-inbox\inbox.db |
Run bin/agent-inbox paths to see the resolved values on your machine.
| Field | Cap | Counted as |
|---|---|---|
| Subject (send / reply) | 500 chars | Unicode code points |
| Body (send / reply) | 1,000,000 chars | Unicode code points |
| Search subject | 1,000 chars | Unicode code points |
Counts are Unicode code points, not grapheme clusters. A ZWJ family
emoji like 👨👩👧👦 counts as 7 code points; a decomposed accent
(é) counts as 2; the composed form (é) counts as 1. The
Python and Go layers both use code-point counting (Go via
utf8.RuneCountInString) so the boundaries match across CLI, MCP
server, and desktop UI.
agent-inbox is designed for a single trusted operator on one workstation. There's no per-agent authentication boundary — any process that can read the SQLite file or open the MCP stdio pipe is treated as authorized. That means:
- Any agent can
inbox_sendclaiming anysendername (no signature, no tokens). The roster is a soft contract enforced by validation, not a security boundary. - Any agent can
inbox_readany message by ID andinbox_searchacross all senders / recipients. - Filesystem permissions on the SQLite file (
mode 600by default on the user's own data dir) are the actual access control. - The desktop UI's
ApproveandRejectbindings have no caller verification — anything that can talk to the Wails webview can call them. That's acceptable here because the webview is owned by the operator's own desktop session; the trust boundary is the OS user. - The operator name (
AGENT_INBOX_OPERATOR, defaultoperator) is validated at startup against the agent-name regex. Reserved names likeallare rejected; misconfiguration fails loudly with a clear error rather than producing a quietly-broken install where any agent could send asall.
These trade-offs are deliberate: it's a coordination tool for one operator's own agents, not a multi-tenant message bus. If you need cross-user or cross-host coordination with real authentication, this is the wrong tool.
Multiple processes — your MCP servers, the CLI, the desktop UI — read and write the same SQLite file. WAL allows unlimited concurrent readers and serializes writers. The discipline:
journal_mode=WAL— readers don't block writers, writers don't block readersbusy_timeout=5000— SQLite waits up to 5s for a contended writer lock before raising an errorsynchronous=NORMAL— durable under WAL, ~5× faster thanFULL- Connection-per-operation — short-lived locks, fast release
- Migration runs once per process per DB path (cached) and uses
BEGIN IMMEDIATEso concurrent fresh processes serialize cleanly - App-level retry helper (3 retries, exponential backoff) wraps writes for
the rare cases
busy_timeoutdoesn't cover
Tested: 20 threads × 5 writes (100 inserts, no losses) and 5
subprocesses × 10 writes (50 inserts via spawn, no losses). The
journal-mode switch from default to WAL (which needs an EXCLUSIVE lock
that busy_timeout doesn't always cover) is wrapped in its own
retry-on-busy helper so simultaneous fresh processes against a brand-new
DB don't race the initial setup.
A brief is plain markdown. The filename (without .md) is the canonical
agent name. Names must match ^[a-z][a-z0-9_-]*$ against the strict end
of string — trailing whitespace or newlines are rejected. The reserved
name all is used for broadcast and cannot be a brief filename.
examples/briefs/ ships a reference seven-role bundle
covering a typical engineering workflow: operator, architect,
implementer, reviewer, failure-analyst, tester, ops. Each is
~80–120 lines describing strengths, what the agent avoids, inputs,
outputs, hand-offs, and when to use it. Read examples/README.md
for the pipeline shape and a quick-start copy command.
The contents are advisory — they describe the agent so other agents (or
you) know what role it plays. Other agents can fetch a brief via
inbox_brief(name) before deciding to contact it.
MCP servers can't push, so the agent has to ask. Three patterns, in order of preference:
-
inbox_waitlong-poll (universal). Brief instructs: "when idle, callinbox_waitfor your name." The tool blocks server-side until mail arrives or the timeout elapses (default 30s). Re-call in a loop to keep polling. Works in every MCP client. -
Client hooks (per-client, optional). For clients that support hooks (Claude Code CLI's
SessionStart/Stop), a hook script can callbin/agent-inbox list --for <name>and inject the result. Hook bundles ship in a future release. -
inbox_checkon demand. Always works as a manual trigger.
git clone https://github.com/KI7MT/agent-inbox.git
cd agent-inbox
uv sync --all-extras
uv run pytestMIT — see LICENSE.