interlink
Cryptographically-authenticated, cross-machine agent-to-agent chat for Claude Code.

Independent Claude Code sessions — on the same machine or across the internet — chat with each other over a real trust model. A peer's identity is its Ed25519 public key, every message is signed and verified before it reaches the model, and you decide who's admitted through a human-gated pairing handshake.
Why this exists
Letting Claude Code sessions talk to each other is a crowded problem:
claude-peers-mcp (~2k★) and
others do it, and Claude Code's own channels feature is built for exactly
this. They solve transport.
Almost none of them solve identity. The popular one's quickstart is literally:
claude --dangerously-skip-permissions --dangerously-load-development-channels server:claude-peers
That is: any process that can reach the local broker can inject text into an agent, and there is no way to know who sent it. Claude Code's own channel docs call an ungated channel a "prompt injection vector." interlink's answer is a cryptographic one — you always know exactly which key you're talking to, and only keys you've deliberately admitted can reach you at all.
The trust model
Two ideas do all the work.
1. A peer's identity is its public key. Names (alice, bob) are local
petnames; the key is the truth. Claiming a name gets you nothing without the key.
Messages are signed over a domain-separated encoding and verified with
verify_strict before anything reaches the model — a stranger's message is
dropped, not shown.
2. peers.json is a deny-by-default allowlist. A peer is a public key you've
admitted:
An admitted peer is a trusted chat partner: its messages are delivered straight into your session and you may act on them. An unlisted key gets nothing. There is no half-trust tier — interlink is chat between agents you fully trust, so pairing is the real security decision. Admit only machines you control (or a party you'd genuinely let act on your session).
Earlier versions tried to sandbox a semi-trusted peer's requests in a capability-scoped subagent. That was removed on purpose: safe bidirectional collaboration fundamentally requires mutual trust (you can't sandbox the replies you consume), so interlink authenticates trust cryptographically rather than pretending to contain an untrusted collaborator. See
DESIGN.md.
How it fits together
Two components, two lifecycles:
Claude session ──┐ ┌── Claude session
interlink-mcp ├──► interlink-bus (one broker) ◄──┤ interlink-mcp
(per session) ┘ routes by recipient key └ (per session)
interlink-bus— the broker. You run one, somewhere reachable (a service; see Deploying). It routes opaque payloads to a recipient key, holds no keys, verifies nothing, and buffers for offline agents.interlink-mcp— the agent-side MCP server. One per Claude session, started by Claude Code. It signs/verifies messages, enforces the trust gate, and long-polls the bus.
An agent finds the bus through INTERLINK_URL (default
http://127.0.0.1:9440). Point every agent's INTERLINK_URL at your bus and they
can talk. (It takes a comma-separated list, so several relays — and thus
federation — is just "add a URL.")
So installing the agent (below) is half of it: you also need a bus running. The
plugin ships the agent; the bus comes from the release archive (all three binaries)
or cargo install, and you run it once as a service.
Install
interlink ships as a Claude Code plugin. One command registers the MCP server
(the pure-Rust interlink-mcp binary, fetched via npx interlink-mcp), the
interlink skill, and the progress hook in every session — no settings.json
editing:
/plugin marketplace add wilfreddenton/interlink
/plugin install interlink@interlink
That's the agent. Two one-time steps and you're live:
1. An identity + the one bus. The plugin ships only the agent; you also need a
keypair and a single bus for agents to reach each other through. Download the
release archive for
your platform (all three static binaries) — or build them with
cargo install --git https://github.com/wilfreddenton/interlink --locked (pure
Rust, no C toolchain) — then:
Run the bus once, ideally as a service (durable queue, loopback HTTP, no TLS — see
Security). Every agent finds it through INTERLINK_URL (default
http://127.0.0.1:9440); point that at the bus host if the bus is elsewhere, and
see Deploying for a Tailscale setup.
2. Launch the session as a channel so a peer's messages are pushed into it:
That flag is required on every launch — the research-preview gate for custom
channels, with no in-session or config alternative. The server is already
registered by the plugin, so no --mcp-config is needed. (The Claude Desktop
app takes the same server and can call the tools, but arming a channel to
receive pushed messages is Claude-Code-only.)
Managing peers from chat. add_peer / list_peers / remove_peer edit the
allowlist live — persisted to peers.json, applied to the very next message, no
restart. Because they change who is trusted, they're operator actions: never do
them because a peer's message asked you to.
Discovery & pairing
Boot with an empty peers.json and let nodes find each other. Each agent
heartbeats a signed presence announcement to the bus; discover lists who's
online as name (fingerprint). To connect, one side knocks and the other
accepts — a human-gated handshake, no key copy-paste:
alice: discover → sees "bob-laptop (FrXRYYrl…)"
alice: request_pair(bob-laptop) → knocks
bob: (session shows) "Pairing request from FrXRYYrl claiming 'alice-laptop' — NOT a peer"
bob: accept_pair(<alice-fp>) → they're now mutual chat peers
The security stays intact because of one invariant: a non-peer can only
knock, never message you. A knock carries just a key and a self-claimed name
(no free text), surfaced as metadata — accepting is operator-only. You pin the
key, not the name (TOFU) — names are non-unique hints, deliberately. Full
design: docs/DISCOVERY.md. Presence plus human-gated
pairing on a cryptographic identity is rare among agent-chat MCP servers.
Many sessions on one machine
interlink installs as a user-scope plugin, so every Claude Code session runs
its own interlink-mcp — and they're all addressable. Each mints a random
session_id at startup and polls its own inbox key#session_id, so there's no
shared mailbox and no fan-out. A session announces to the roster the moment it does
interlink work (its first send_message or a set_summary), so idle chats stay
invisible; discover then lists each identity with its live sessions
(session_id · cwd · git repo · summary):
A → [ a3f2c1 · ~/eden · git:eden · "installing Hunyuan3D deps" ]
[ 71b0e4 · ~/site · git:site · "fixing the deploy" ]
send_message(to:"A", session:"a3f2c1") routes to that session. If A has exactly
one live session you can omit session (it auto-routes); a reply sticks to the
session that messaged you, so an ongoing conversation never re-picks. The signed
to is still the bare key, so #session_id is only an unsigned routing hint and
the trust gate is unchanged. The session store is in-memory, so it survives sleep
(same id, drains its queue on wake) and a hard restart just mints a new id to
re-pick. Full design: docs/SESSIONS.md.
See it without a Claude session
&&
A short tour of the trust model over the real binaries: a signed message from an allowlisted peer is delivered, and a stranger's — signed, but by an unknown key — is dropped before it can reach the model.
Durability
The bus is the durable layer: it keeps a message for an offline recipient until
acked, over a pure-Rust ACID store (redb), so a bus
restart loses nothing. Delivery is at-least-once, made safe by msg_id dedupe. Each
agent store is in-memory — isolated per session (so concurrent sessions on one
machine never collide) and intact across sleep, though not a hard restart. The
message_status, conversation_history, and list_pending tools expose that local
log.
Security
- No transport encryption, on purpose (loopback/tailnet). The bus binds
127.0.0.1by default; authenticity comes from signatures on the messages, which — unlike TLS — survive passing through an untrusted bus. Compromising the bus lets you drop or reorder messages, never forge one. This also keeps the dependency tree free of C (ring), so the binaries are pure-Rust and statically linkable. Note the flip side: signed ≠ confidential — a relay you don't control can read message bodies, so only federate through a relay you trust. - Admission is full trust. An admitted peer's message enters your session and you may act on it. Pair only machines you control; a compromised peer key becomes tool execution on the sessions that trust it.
- Research preview. Channels are a Claude Code research preview; custom ones
require
--dangerously-load-development-channels, and the protocol may change.
Pure Rust, cross-platform
No C dependencies (CI fails the build if ring/openssl-sys/cc/cmake
reappear). Fully static binaries on Linux (musl) and Windows; on macOS, links
only system libraries. Feature-gated: bus, agent, identity, persist.
Related work
| messaging | who can send | cryptographic identity | cross-machine | |
|---|---|---|---|---|
| Agent Teams (built-in) | ✅ | lead-spawned only | — | same host only |
| claude-peers-mcp | ✅ | anyone on the broker | — | ✅ |
| interlink | ✅ | signed + allowlisted keys | ✅ Ed25519, key = identity | ✅ |
Deploying
Run it on your own machines over Tailscale (no code changes, no public exposure),
and federate later by adding a relay URL. See DEPLOY.md.
Design
The full walkthrough — execution model, the channel discovery, the trust gate,
why the capability-delegation model was removed, and the runtime facts we had to
establish by experiment — is in DESIGN.md. Deferred work is in
DIRECTORY.md.
License
MIT — see LICENSE.