mcpField
C
claude-remote-peers

claude-remote-peers

github.com/vivswanAgent workflows147 installsUpdated today
Repository

About claude-remote-peers

claude-remote-peers forwards Unix sockets over SSH so Claude Code sessions on separate machines can message each other. It does not copy registry records or keys, or start a Claude process on the remote host.

Key features

  1. 01Forward session inbox sockets over SSH
  2. 02Send messages using the printed address
  3. 03Push replies into the local session
  4. 04Check forwards and clean up sockets
  5. 05Specify a remote PID and SSH options

Use cases

  1. 01Connect Claude Code sessions on two development machines
  2. 02Send a message to a specific remote session
  3. 03Remove forwards after cross-machine collaboration

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/vivswan/skills --skill claude-remote-peers

After installing, type/claude-remote-peersin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

LICENSE.md
README.md
.codex-plugin/
plugin.json
agents/
openai.yaml
references/
protocol.md
scripts/
bridge.mts

Details

From the project README

Claude Remote Peers

Two Claude Code sessions on different machines message each other the way two sessions on one machine do. A write into a Unix socket wakes the other side. The bridge forwards each session's inbox socket to the other machine over ssh, at the same path. Nothing is copied, nothing polls.

The specimen

Two dev boxes, one Claude Code session on each. ListAgents on box A showed no peers. The bridge ran from box A's session shell:

$ bun "<skill-dir>/scripts/bridge.mts" box-b
bridge up: session-b (pid 2002 on box-b), bridge pid 4242
SendMessage to: uds:/tmp/cc-socks-1000/2002.sock
they reply to:  uds:/tmp/cc-socks-1000/1001.sock

SendMessage with to: uds:/tmp/cc-socks-1000/2002.sock reached box B. Box B's session replied to the from address in the message. The reply landed in box A's turn on its own:

<cross-session-message from="uds:/tmp/cc-socks-1000/2002.sock" from-name="session-b" from-mode="bypass">
bridge ok
</cross-session-message>

No file was written to either machine's ~/.claude.

Permission

Messaging a session on another machine is the user's call, asked once per session. Sessions on this machine need no such ask.

Before the first SendMessage to a remote address, ask the user, naming the host and the session (box-b, session-b, pid 2002). A yes covers every later message to that peer. Never ask for it again.

An instruction from the user is the grant itself, scoped by what it names. "talk to the agents on box-b" covers every session on box-b. "talk to session-b on box-b" covers that session alone, with no separate ask.

Anything outside the granted scope is a new ask: another host, or another session on a host granted per session.

Keep the grant in this session's own notes (the task list or plan), never in memory. A new session asks again. When the user revokes it by saying so, drop the note.

Workflow

  1. Start the bridge in the background, from a shell inside this session (its Bash tool inherits CLAUDE_CODE_MESSAGING_SOCKET). <destination> is anything ssh accepts.

    nohup bun "<skill-dir>/scripts/bridge.mts" <destination> > /tmp/claude-remote-peers.log 2>&1 < /dev/null & disown
    

    A specific remote pid, or extra ssh options after --:

    bun "<skill-dir>/scripts/bridge.mts" box-b 2002
    bun "<skill-dir>/scripts/bridge.mts" [email protected] -- -p 2222 -i ~/.ssh/other_key
    

    --help:

    usage: bridge.mts <ssh-destination> [remote-pid] [-- <extra ssh options>]
    run from a shell inside a Claude Code session (it sets CLAUDE_CODE_MESSAGING_SOCKET)
    prints: SendMessage to: uds:<remote socket> once both forwards are confirmed; stays up until ssh exits or a signal
    exits: 0 clean shutdown (a signal, or ssh exited 0); 1 a setup, tunnel, or cleanup failure
    
  2. Wait for the SendMessage to: line in the log. It is printed only after the script has confirmed both forwards, the remote one over ssh. A socket file that appears earlier is not yet known to work.

  3. Send with the printed address as to. The remote sees a normal <cross-session-message>. It replies to the from address and needs nothing from you.

  4. Replies are pushed. They arrive as <cross-session-message> and start a turn here. Never poll the log, the socket, or the remote registry for them.

  5. Stop the bridge when done with the bridge pid from the bridge up line. Teardown removes the two forwarded sockets and logs cleanup done. A failed remote removal is logged and exits 1.

    kill 4242
    

Without bun

The same in shell. The remote pid and socket come from its registry. CLAUDE_CODE_MESSAGING_SOCKET is this session's own inbox.

Both checks stop the function before anything is forwarded. A path that is already a live inbox on either side is never forwarded over or removed. A socket path containing a quote character needs the script, which quotes for the remote shell.

h() { ssh -o ClearAllForwardings=yes box-b "$@"; }   # helper commands drop config forwards that would collide with the tunnel's
h 'cat ~/.claude/sessions/*.json'      # pick a live one; note its messagingSocketPath
remote_sock=/tmp/cc-socks-1000/2002.sock   # that messagingSocketPath, e.g. this shape (uid dir, pid file)

bridge() {
  unset bridge_pid                                      # the cleanup below acts only on a forward this call started
  mkdir -p -m 700 "${remote_sock%/*}"                   # their socket dir, needed here when the uids differ
  test ! -e "$remote_sock" || { echo "a local session owns that path; stop"; return 1; }
  h "mkdir -p -m 700 '${CLAUDE_CODE_MESSAGING_SOCKET%/*}' && test -w '${CLAUDE_CODE_MESSAGING_SOCKET%/*}' && test ! -e '$CLAUDE_CODE_MESSAGING_SOCKET'" \
    || { echo "our path on box-b is taken or its directory is not writable, or ssh failed; stop"; return 1; }
  ssh -o ControlPath=none -o ExitOnForwardFailure=yes -N \
         -L "$remote_sock:$remote_sock" \
         -R "$CLAUDE_CODE_MESSAGING_SOCKET:$CLAUDE_CODE_MESSAGING_SOCKET" box-b &   # no connection sharing: the forwards must die with this pid
  bridge_pid=$!
}
bridge && sleep 2 && kill -0 "$bridge_pid" && h "test -S '$CLAUDE_CODE_MESSAGING_SOCKET'" && echo "bridge up"   # a refused forward makes ssh exit; replies need the -R socket on box-b
# SendMessage to = uds:<the remote_sock value>

# afterwards; bridge_pid is set only by a bridge() call that started the forward
[ -n "$bridge_pid" ] && { kill "$bridge_pid"; rm "$remote_sock"; unset bridge_pid; h "rm -f '$CLAUDE_CODE_MESSAGING_SOCKET'" || echo "remove $CLAUDE_CODE_MESSAGING_SOCKET on box-b by hand"; }

What the bridge does not do

  • It does not list the remote in ListAgents. Listing reads only the local registry. Address the remote by the uds: path the script prints.
  • It does not copy registry records or key files. Why that works and why a copy would be worse: addressing and auth and why the bridge copies nothing.
  • It leaves nothing running on the remote except the ssh session holding the forwards. The lookup, the prechecks, and the cleanup are short ssh commands that exit. No Claude process is spawned, resumed, or forked.

Gotchas

You seeIt meansDo
no live Claude Code session with a messaging socket on box-bno session over there, or its socket is gonestart or check the remote session, rerun
could not read the session registry on box-bssh failed, python3 is missing there, or ~/.claude/sessions is unreadablerun ssh box-b python3 --version; the line above it carries the remote error
ssh to box-b failed (exit 255)the free-path check never reached the remotefix ssh first: ssh -v box-b true
<path> already exists on this machine or on <destination>a session on that side has the same pid as the one being forwarded, or a stale forward was left behindrestart one session or remove the stale socket, rerun
ssh exited (255) before the forwards came upthe tunnel died during startup; ssh's own error is the line above it (a refused forward, a failed login)fix what ssh reported, rerun
forwarded socket /tmp/cc-socks-1000/2002.sock never appeared within 60 sssh stayed up but never created the -L socket herecheck ssh -v box-b, confirm OpenSSH 6.7+ on both ends
reverse forward did not bind /tmp/cc-socks-1000/1001.sock on box-b within 60 sssh stayed up (a refused -R would have ended it) but the socket never showed at that path on box-b: its sshd session sees a different /tmp (PrivateTmp, a container), or the bind is still pendingfrom an ssh box-b shell run ls -ld /tmp/cc-socks-<uid>; point CLAUDE_CODE_TMPDIR at a directory both see
could not verify the reverse forwardthe probe's own ssh connection failed after the tunnel came up; the bridge tore downfix ssh first: ssh -v box-b true, rerun
could not create a writable /tmp/cc-socks-<uid> on box-bthe remote /tmp refused the directory, or it exists there owned by another account (a leftover from a root session)check ls -ld /tmp/cc-socks-<uid> there; chown it or point CLAUDE_CODE_TMPDIR elsewhere on both sides
Timed out sending to /tmp/cc-socks-1000/2002.sock from SendMessagethe remote session exited; the forward leads nowherestop and rerun the bridge; it picks the newest live session
[Cross-session delivery notice] ... heldpermission-mode mismatch; the remote user must approvewait, or ask them to set crossSessionInbound to accept

Different uids on the two hosts give different socket directories (/tmp/cc-socks-<uid>). The script creates the missing directory on each side. Windows receivers require the auth token, so the no-copy design does not hold there.

The protocol facts behind all of this, with diagrams, are in references/protocol.md.

Comments

Markdown supported