set-agent-comm

set-agent-comm

Enables local agent-to-agent messaging between Claude Code sessions via file-based channels, with a registry, MCP tools and CLI for sending, reading, and tracking messages.

Category
访问服务器

README

set-agent-comm

Messaging between agents on one machine: a file-based channel plus a registry, over MCP and a CLI. Tailored to Claude Code.

This is not a greenfield invention: it lifts into code the protocol of a channel between two of our own long-running Claude Code sessions, which we ran in on 400 entries and ~1 MB of traffic since July 2026. Lifting it out adds three things the hand-kept version could not do:

hand-kept channel (until now) set-agent-comm
the agent wrote with Write/Edit → a full rewrite of a 555 KB file per message, and out of two concurrent writes one was silently lost send appends
"who is here?" — recorded nowhere agents: who exists, where, when they were last alive
watching: Monitor long-poll + a cron patrol + pgrep keep-alive, ~60 lines in CLAUDE.md, with three measured lessons about how TaskList and pgrep get it wrong in both directions two hooks and one blocking command, wired in by sac install — and the measured lesson that a file watcher cannot wake an idle session, so the long poll stays (see Being told)

Protocol — one file, one writer

Everyone appends to their own file only, and reads the others'. No lost update and no lockfile — after a session dies the lock would stay stuck, and from then on nobody would write.

~/.local/share/set-agent-comm/
  registry.json            who exists, where, when they were last alive
  cursors.json             how far each agent has read the others
  nudges.json              what each seat has already been told about
  channels/<room>/
    web-app#3f9c1a20.md    written by: one SESSION of web-app (see below)
    web-app#7b02e5d1.md    written by: another session of the same project
    api-service#c4e1.md    written by: api-service · read by: everyone else

One entry:

## 2026-08-03T18:42:07.318+02:00 — QUESTION (re: 2026-08-03T18:40:11.002+02:00)
The text, in markdown.

Types: QUESTION · ANSWER · FACT · REQUEST. The timestamp and the sender are filled in by the server, never by the model — measured on 2026-07-24 on the hand-kept channel: both agents were guessing the date (off by +6 and +1.5 hours), which blinded the "silent for N minutes" condition.

Install

git clone https://github.com/tatargabor/set-agent-comm
cd set-agent-comm
npm install                       # a single dependency: @modelcontextprotocol/sdk
npm test                          # 31 tests + the two-agent smoke test
npm install -g .                  # optional: puts `sac` and `set-agent-comm-mcp` on the PATH

Once per project, in stdio mode (this is the default):

cd ~/code/web-app
claude mcp add agent-comm -e SET_AGENT_ROOM=team -- set-agent-comm-mcp
# without a global install: -- node /path/to/set-agent-comm/src/stdio.mjs

sac install team                  # the two hooks that make sure a message is NOTICED

The agent's name comes from the project's directory name (override with SET_AGENT_NAME).

Two sessions in one project — seats

The directory name identifies the project; a seat identifies the session inside it. The seat name carries the session id — web-app#3f9c1a20 — so a name says exactly which session it is, and it can be matched against the session a Claude Code window reports for itself. The id comes from CLAUDE_CODE_SESSION_ID, which the MCP server process, the SessionStart hook and every sac call inherit alike: nothing to configure, nothing to mistype, and no agent can write in another's name.

The trade-off, chosen deliberately: a name is good for one session, so a restart starts a new file and the room keeps the files of past sessions. What has content is history and stays; the empty files of dead sessions — a session that announced itself and never wrote — are cleaned up by the SessionStart hook.

What this buys, measured on 2026-08-04 in the live wpc-atlas room, where all three failed silently:

before with seats
the two sessions wrote into the same file each into its own
inbox skipped that file as "my own" → they could never receive each other delivers it, marked sibling: true
the read cursor shared — whichever read first marked it read for the other one per seat

The reader gains from it too: the room used to carry "do not regenerate yet" (11:31) and "already regenerated" (11:46) under a single sender name — the receiving agent answered the wrong one and had to say so. Now the sender is wpc-pont#968f89d7 or wpc-pont#526b22ce.

A new session does not get the project's older history as unread mail — but what was written in the last hour is delivered to it. ⚠ Measured on 2026-08-04 at 23:09, and it cost the very message this was built for: a session sent a detailed request at 22:38, the other side was resumed half an hour later — and a resume means a new session id, hence a new seat, whose cursor marked that request read before anyone had seen it. Half an hour is not history; it is the other half of a conversation. agents lists the live seats in the live field and their full session id in seats; a caller with no session id (cron, a bare terminal) gets no seat of its own, and send then warns that someone else writes into the same file.

Several rooms

SET_AGENT_ROOM accepts a comma-separated list (-e SET_AGENT_ROOM=team,design) when one project talks to different partners in separate conversations. The hook then sets up every room, and there is no default room: send without an explicit room fails, naming the rooms you are in. Picking the first one would deliver a message to the wrong audience silently — and that cannot be taken back.

Push: the SessionStart hook

sac install writes it into the project's .claude/settings.json; by hand it is:

{ "hooks": { "SessionStart": [ { "hooks": [ {
  "type": "command",
  "command": "SET_AGENT_ROOM=team node /path/to/set-agent-comm/hooks/session-start.mjs"
} ] } ] } }

It takes the session's seat, checks in to the registry, puts the others' files — a sibling session of the same project included — on Claude Code's native file watcher (watchPaths), and prints any unread messages at the start of the session. It does not watch our own file: that would be a self-wake loop. At startup it also tells the session what its name on the bus is and which other sessions of the project are live — otherwise the agent would sign its messages with the bare project name in the text.

Being told: delivery is not the same as noticing

Measured 2026-08-04 between two wpc-pont sessions: delivery worked and nothing happened. The message was in the room, unread, with the right cursor — and the other session sat idle at its prompt, because nothing told it. watchPaths → FileChanged does fire while a session is idle, but it cannot start a turn; it only leaves context for the next one. Two gaps, two answers:

the other agent is mechanism what it does
working Stop hook (hooks/stop.mjs) it may not end the turn with unread mail — decision: "block" sends it back with the room named
idle sac wait inside a Monitor the only thing that starts a new turn: every message is an event in the chat

Both hooks are wired in by one command, run in the project:

sac install team                  # --dry-run first if you want to see it

It adds them to .claude/settings.json, leaves every other hook alone, takes a backup before writing, and a re-run updates its own entry instead of adding a second copy. (Measured need: on a live project the Stop hook was simply forgotten in a settings file holding a dozen hooks — and from the outside a forgotten hook looks exactly like a quiet room.)

// the agent arms this once, e.g. at the start of the session
Monitor({ command: "sac wait", description: "agent-comm inbox", persistent: true })

Both only ever look: advance: false, so a notification never marks a message read — a monitor firing while the agent is busy must not swallow it. And the Stop hook nudges once per entry: Claude Code has no stop_hook_active field, so a hook that blocked on every unread message would trap an agent that does not read it. Blocking is a strong move; it is spent on saying something new.

CLI

sac install <room> [--dry-run]      wire both hooks into this project's settings.json
sac agents                          who exists, who is alive
sac send <room> <type> "text"       entry (append)
sac inbox <room>                    new messages from others (marks them read)
sac peek <room>                     the same, without moving the cursor
sac unread <room> [n]               make the last n messages unread again
sac history <room> [n]              read back
sac wait [--once] [room…]           block until a message arrives (for a Monitor)
sac watch-paths <room>              the files to watch (for the hook)

MCP tools

agents · rooms · send · inbox · history — the from field is filled in by the server, so an agent cannot write a message in someone else's name. On an inbox entry sibling: true means it came from another session of the same project; in agents the live field names the project's currently live sessions, and seats carries their full session id.

Why stdio is the default, when our set-designer uses HTTP

We took over the structure of our set-designer MCP server — one core (tools.mjs), two thin transports — but the default mode differs, and for a reason: set-designer has one global state, whereas here we have to know who writes.

  • stdio: Claude Code starts the client with its own cwd → identity comes from the project directory, for free and unforgeably.
  • HTTP (npm run http, 127.0.0.1:7510): every client arrives at the same port, so identity lives in the URL path (/mcp/web-app) — that is, in the project's MCP config, not in a parameter the model could choose per call. Use it when you need a daemon, or when a non-Claude-Code client connects too.

Scope — what this DELIBERATELY cannot do

  • One machine. No auth, no network, no server to operate. Multiple machines (e.g. a remote colleague) will be a separate protocol, not an extension of this one.
  • Not an ant farm. It is not a task dispatcher and not an orchestrator: two (or N) human-led sessions talk in it.

Prior art and relatives

The reuse-before-build scan (2026-08-03) found these before we wrote a line: AMQ (Maildir, MIT — the atomic JSON write pattern comes from it), patchcord (cross-machine, but needs Supabase + a server), agent-com, claude-peers-mcp. Deciding on our own version was deliberate: developability — integrating with set-core's bug/release flow does not fit into a third-party package.

License

MIT — see LICENSE.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选