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.
It runs on plain claude — no --dangerously-load-development-channels and no
org channelsEnabled, both of which Claude Code channels require. Delivery defaults
to a channel-less path (a background listener that wakes on incoming messages), with
native channels as an opt-in enhancement (interlinked) where you have them.
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 four binaries)
or cargo install, and you run it once as a service.
Install
interlink ships as a Claude Code plugin. Installing it registers the MCP server
(the pure-Rust interlink-mcp binary, fetched via npx interlink-mcp), the
interlink skill, and the hooks (progress-nudge + the channel-less inbox listener)
in every session — no settings.json editing.
From the claude CLI:
Or the same two commands as slash commands inside a session (/plugin marketplace add wilfreddenton/interlink, then /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 four 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 — plain claude is all you need. Just run claude; the plugin
delivers incoming messages over the channel-less fallback: the server writes each
verified message to a local inbox that a background interlink-mcp wait task drains,
and a Stop hook keeps that listener armed. No flags, and it works even where Claude
Code channels are disabled by org policy.
If you do have Claude Code development channels and want the nicer native push,
launch with interlinked instead of claude:
That sets channel mode (the server pushes directly; the Stop hook self-disables) and
passes the research-preview flag. Extra args forward to claude. Same trust model
either way — channels vs. background-task is only how a message reaches the model.
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 registers on startup (node + session;
node registration is idempotent — the bus groups sessions under one pubkey) and
unregisters on close, so the roster reflects your currently-open sessions.
discover 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.
Two sessions on the same machine share one identity, so they can talk with
send_message(to:"self", session:"<id>") — no pairing and no self-entry in
peers.json, because it's the same principal (only the holder of your key can sign
as it). A session can't address itself: it's excluded from discover routing and
an explicit self-target is refused. 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.
- Delivery is channel-optional. The default fallback (local inbox + background
wait+ Stop hook) needs no special flags and works under any org policy. Native channels are an opt-in enhancement (interlinked) and a Claude Code research preview — custom ones require--dangerously-load-development-channelsand the protocol may change. The trust gate is identical on both paths.
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.