peer-channel
Lets multiple Claude Code sessions talk to each other locally. Each session registers under a name and can then list peers and exchange messages.
Architecture
CC session A CC session B
| |
| stdio (MCP) | stdio (MCP)
v v
peer-channel peer-channel
(~/.peer-channel/ (~/.peer-channel/
sessions/A.sock) sessions/B.sock)
| peer-to-peer |
+------- AF_UNIX + NDJSON ----------+
Each session's channel subprocess claims a name by acquiring a lockfile at ~/.peer-channel/sessions/<name>.lock and binds a Unix domain socket at ~/.peer-channel/sessions/<name>.sock. Messaging is direct peer-to-peer: the sender opens a one-shot connection to the recipient's socket, writes one NDJSON JSON-RPC request, reads the response, closes.
No central daemon. No Docker. No open TCP port.
Requirements
- Node.js 20+
- Claude Code v2.1.80+ with claude.ai login (channels are in research preview)
- On Team/Enterprise plans, an admin must enable channels
- Linux or macOS (Windows support is not yet wired up)
Install
From within Claude Code:
/plugin marketplace add rophy/claude-peer-channel
/plugin install peer-channel@rophy-plugins
Alternatively, register the channel directly via ~/.claude.json:
{
"mcpServers": {
"peer-channel": {
"type": "stdio",
"command": "node",
"args": ["/absolute/path/to/claude-peer-channel/plugin/channel.js"]
}
}
}
Usage
Launch any Claude Code session with the channel enabled:
claude --dangerously-load-development-channels plugin:peer-channel@rophy-plugins
# or, if you registered via ~/.claude.json:
claude --dangerously-load-development-channels server:peer-channel
The --dangerously-load-development-channels flag is required during the channels research preview until peer-channel is on the approved allowlist.
On startup, the channel reports its registered name to stderr:
[peer-channel] registered as: my-project
Tools exposed to Claude
list_sessions — returns the names of all other sessions currently reachable.
send_message(to, text, in_reply_to?) — sends a message to another session. Pass in_reply_to with a prior message's id to thread replies.
Inbound messages
Messages from peer sessions arrive in the receiving Claude's context as:
<channel source="plugin:peer-channel:peer-channel" from="peer-name" message_id="uuid" in_reply_to="optional-uuid">
message body
</channel>
(When loaded via a plain mcpServers entry instead of the plugin marketplace, the source attribute is server:peer-channel instead. The from, message_id, and in_reply_to attributes are stable across both install modes.)
Session naming
By default, a session's name is basename(cwd). If that name is already claimed by another live session, the channel appends a short random suffix.
Override with an environment variable:
PEER_CHANNEL_SESSION_NAME=backend-api claude --dangerously-load-development-channels server:peer-channel
Environment variables
| Variable | Default | Description |
|---|
PEER_CHANNEL_SESSION_NAME | basename(cwd) | Override session name |
Protocol
Newline-delimited JSON-RPC 2.0 over an AF_UNIX stream socket. One request per connection.
| Method | Params | Result |
|---|
ping | {} | {name, version, protocol} |
deliver | {from, text, in_reply_to?} | {message_id} |
Error codes follow JSON-RPC conventions (-32700 parse, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal).
Design decisions
- No offline delivery. If the target session isn't reachable,
send_message returns an error.
- No presence push. Sessions don't receive join/leave events; call
list_sessions on demand.
- Per-user trust. The sessions directory is
mode 0700 and sockets are mode 0600 — only the user that owns the home directory can interact with the channel.
- Peer messages are untrusted input. Another session's text is treated as a user-like request, not as instructions to Claude.
- Name claim via
proper-lockfile. Stale locks are auto-reclaimed after 10s; the owner refreshes every 5s while alive. A crashed session becomes reclaimable within that window.
Filesystem layout
~/.peer-channel/
└── sessions/
├── alice.lock/ # directory, created by proper-lockfile
├── alice.sock # AF_UNIX socket
├── bob.lock/
└── bob.sock
Development
npm install
npm run dev:channel # run channel standalone (useful for debugging)
npm run build # compile to dist/
npm run build:plugin # bundle channel into plugin/channel.js (committed)