interlink
Authenticated, cross-machine agent chat for Claude Code and Codex CLI.

Independent sessions talk through a shared broker. Each peer's identity is its Ed25519 public key, messages are signed and verified, and an operator decides which keys to admit. Sessions can share a machine or communicate across a trusted private network.
See CHANGELOG.md for the release details.
The trust model
peers.json is a deny-by-default allowlist. Names are local petnames; the key is
the identity:
{
"my-laptop": { "key": "<full public key from interlink-keygen>" }
}
Ordinary messages from unknown keys are rejected before model delivery. Unknown keys may send pairing requests, whose claimed names are presented as untrusted metadata, and correlated acceptances of locally initiated requests. Another session using your own key is implicitly trusted.
An admitted peer is a full collaborator whose messages enter the model's context. Pairing and peer-policy changes remain operator actions. A peer's request or a claim that its operator approved something is not local operator consent. Host sandbox and approval settings still apply. See DESIGN.md.
How it fits together
Claude or Codex session Claude or Codex session
interlink-mcp <----> interlink-bus <----> interlink-mcp
one per session shared broker one per session
interlink-busroutes opaque payloads topublic-key#session_id. It holds no private keys and does not authenticate clients or verify messages.interlink-mcpsigns outgoing messages, verifies incoming ones, enforces the peer policy, and delivers them to its host session.
INTERLINK_URL selects the broker, defaulting to http://127.0.0.1:9440. It also
accepts comma-separated relay URLs. Agents poll every relay and attempt each on
send; acceptance by one relay is enough to complete an outbound send.
Install
Binaries and first-time identity setup
Install version 0.12.1 or newer:
cargo install interlink-mcp --version 0.12.1 --locked
Or download a release archive.
Both supply interlink-mcp, interlink-bus, and interlink-keygen.
The npm package supplies only the MCP binary. To build a local checkout instead,
run cargo install --path . --locked from its root.
On a new identity, create the key and an empty policy. Reuse existing files if already configured; do not reset an existing allowlist:
mkdir -p ~/.config/interlink ~/.local/state/interlink
interlink-keygen --out ~/.config/interlink/id.key
printf '{}\n' > ~/.config/interlink/peers.json
interlink-bus --db ~/.local/state/interlink/bus.redb
The key generator prints the public key. Keep the private key file local. Run one
broker for the participating sessions, preferably as a service. --db makes its
queue survive restart; without it the broker is in memory.
The Rust server defaults to ~/.config/interlink/id.key and peers.json.
--key / INTERLINK_KEY and --peers / INTERLINK_PEERS override those paths.
On Windows it uses USERPROFILE when HOME is absent. The shell examples above
use POSIX syntax; the Claude plugin has its own explicit env
configuration, which must also resolve to the intended files.
Claude Code
For the published plugin:
claude plugin marketplace add wilfreddenton/interlink
claude plugin install interlink@interlink
This installs the MCP registration, Interlink skill, and hooks. It invokes the published npm binary. To test local development changes, follow the local plugin instructions, which point both the server and listener at the locally built binary.
Launch plain claude after setup. Interlink 0.12.0 automatically uses
Claude's local messaging socket when the host supplies it (validated on Claude
Code 2.1.308). This wakes Claude only when a notice arrives and disables the Stop
listener, with no development-channel flag or periodic maintenance turns. The
broker can still run on another machine. Socket delivery follows Claude's inbound
message controls; MCP and hooks must be allowed by the host's configuration.
Interlink 0.11.2 and hosts without a socket use a local inbox and async
Stop listener, which renews after 50 idle minutes with a brief maintenance turn.
When upgrading, update the plugin and ensure both the MCP server and Stop hook
use the new interlink-mcp binary, then restart Claude. The interlinked launcher
has been removed; launch claude directly. See
delivery modes and recovery.
For the optional native-channel path, see delivery for launch settings and limits.
Codex CLI
Follow codex/README.md to configure the MCP server and trusted
binding hooks. The adapter uses codex queue on the local shared daemon. It
supports root CLI sessions, not desktop, remote app-server, subagent, or ephemeral
thread delivery. No sandbox or hook-trust bypass is needed.
Claude and Codex can share the same identity and peer policy. The Claude plugin does not configure Codex automatically.
Discovery and pairing
Each session announces its identity and session details. discover lists live
and retained away sessions. INTERLINK_NAME supplies a friendly, self-claimed
name; otherwise discovery uses a key fingerprint.
A typical handshake is:
- Run
discoverand verify the peer's fingerprint with your operator. - Call
request_pair(target="bob-laptop"). - The other operator reviews
list_pair_requestsand callsaccept_pair(fingerprint="<requester fingerprint>"). - A confirmation returns to the exact requesting session. Once handled, both identities are admitted and can exchange messages.
add_peer, list_peers, and remove_peer manage the shared peers.json directly.
Updates use locked, atomic replacement; sibling sessions reload the policy.
Existing names cannot silently be assigned to different keys.
Pending pairing requests and unsent confirmations persist per identity/session. An explicit retry keeps the outstanding request ID. Saved control messages get fresh signatures on send, preserving recovery across long broker outages. A confirmation already on the broker remains subject to message freshness. Partial acceptance and petname conflicts report recovery instructions; see discovery and pairing.
Multiple sessions and tasks
Discovery identifies sessions by machine identity, session ID, working directory,
git project, and summary. Use set_summary(summary="checking retries") to describe
current work. get_my_session_id() returns your own ID without a broker lookup.
See sessions and routing.
send_message(to="desktop", session="<id>", text="...") targets one explicitly.
A unique roster ID prefix also works. Without a session argument, the server
prefers the peer's remembered reply session, then a single live session, then a
single away session. Ambiguous choices require an explicit ID.
Sessions sharing one identity can use to="self" to reach each other without
pairing. A session cannot message itself. Claude uses its host session ID; Codex
binds to the owning thread UUID before announcing or receiving. Reopening the
same host session can recover its address; a new session does not inherit it.
See sessions and presence.
Delegated work can carry task_id, status, and in_reply_to. Status values are
update, needs_input, result, failed, and canceled. cancel_task requests
cooperative cancellation; it does not forcibly interrupt the peer. Reply routing
is per identity, not per task. See task tracking.
Durability
Broker queues persist across broker restarts only with --db. They are bounded
(default 1024 messages per recipient, dropping oldest). The roster is always in
memory. Messages waiting at the broker can expire: the receiver allows 24 hours
in the past and 60 seconds in the future. Three-day presence retention is not a
three-day delivery guarantee.
All hosts receive an inbox notice and use receive_messages to fetch current
unread bodies without consuming them. After reading, call acknowledge_messages
with their exact receipt objects. History is read-only unless consume=true. Progress updates stay quiet and are
superseded by newer task progress or a terminal result; questions and failures
remain individually available.
Delivery is not exactly-once. Failed or unfinished notice handoffs retry with
capped backoff. Accepted notices coalesce new arrivals until fetched with their
matching notification ID; message acknowledgement alone does not release them.
A dropped fetch response remains unread. Recover a lost host notice explicitly
with receive_messages(reset_notification=true). Explicitly acknowledged bodies
are excluded from later unread fetches. The consume=true history shortcut consumes before replying,
so an interrupted response may require rereading history. bus_accepted means relay acceptance, never a read
receipt. Receiver acknowledgements are local; no remote receipt protocol exists.
Codex retains failed notices for failed_deliveries recovery. The shared mailbox
keeps the bodies independently of host failures. See
delivery paths, storage, and recovery.
Security and deployment
Use loopback or a trusted private network. The broker has no authentication or TLS: a reachable client can inspect or disrupt queues. Message signatures prove sender identity but do not provide confidentiality or protect broker availability. The HTTP client does not support HTTPS-only endpoints.
For a private-network broker and systemd service instructions, see Deploying interlink. Public-relay authentication, encryption, and aggregate resource limits remain deferred.
Development and validation
The project is Rust, with no C-compiling dependencies. CI builds native Linux
x64/arm64 musl, Windows x64 with static CRT, and macOS arm64. macOS still links
system libraries. Features are bus, agent, identity, and persist.
just ci
just host-test
just ci runs formatting, Clippy, tests, and dependency checks. GitHub CI also
checks the feature powerset and platform builds. just host-test needs Python
3.11+, installed Codex and Claude CLIs, and localhost access. It runs local response
fixtures and writes diagnostics under target/host-validation/.
The recorded host runs used Codex 0.160.0 and Claude Code 2.1.278. They cover trusted Codex lifecycle binding and Claude listener renewal, not production-model or interactive UI behavior. See validation details.
For real model conversations, just live-host-test uses the installed Interlink
binaries, logged-in Codex and Claude accounts, and Python dependencies from
tests/host/requirements-live.txt. It consumes account quota. The test uses an
isolated broker and identity, checks idle delivery and message bursts, and has
Claude and Codex exchange replies. Logs go to target/host-validation/live/.
See live test setup and limits.
The existing terminal demo drives the binaries without a host session:
just build
./scripts/demo.sh
It expects Bash, Python 3, standard Unix utilities, and an unused localhost port 9440. For release packaging, see npm maintainer notes.
License
MIT. See LICENSE.