Relay & Hive Mind
Share learnings, delegate tasks, and collaborate across machines — all peer-to-peer, all local-first.
What is it?
The relay connects two or more claudectl instances over TCP. Once connected, they can:
- Share brain knowledge — patterns your brain learns ("always approve
cargo test") propagate to hive members, in both directions - Delegate tasks — offload work to a remote machine running Claude Code
- Synchronize insights — friction patterns, error loops, and accuracy data merge across the network
Every instance stays sovereign. Your local preferences always override peer knowledge. No cloud, no central server.
Quick Start: Connect Two Machines
Step 1: Install
relay and hive are both in the default feature set, so a plain install has cross-machine networking. A build with --no-default-features --features hive keeps hive local: knowledge is distilled, archived, and used by the brain, but never synced to peers.
Step 2: Generate an invite
On Machine A:
Output:
Your identity: laptop-a3f2
RELAY CODE: YEK-AGA-YHK-QAA-BM
INVITE LINK: cctl://laptop-a3f2@192.168.1.50:9847/k/a3f29b1cd4e5f678
Share any of the above with your peer. They run:
claudectl relay join YEK-AGA-YHK-QAA-BM
claudectl relay join cctl://laptop-a3f2@192.168.1.50:9847/k/a3f29b1cd4e5f678
Step 3: Join from Machine B
That's it. Both machines are paired and connected.
Step 4: Start the relay server
On Machine A (the one that generated the invite):
Machine B connects:
Leave both running. The relay is what carries session state between machines, so the cluster view below is only as live as the relays behind it.
Step 5: See the whole cluster
Once relays are up on both machines, every session on either one shows up on both:
Fleet: 5 session(s) across 2 machine(s)
MACHINE PROJECT STATUS COST
────────────────────────────────────────────────────────────────────────
barrys-mac-74371c…* cabal-1 Waiting $137.45
barrys-mac-74371c…* claudectl Processing $31.95
barrys-mac-74371c…* staffai Waiting $17.59
mac-mini-9f2a1b claudectl Processing $4.21
mac-mini-9f2a1b nightly-bench Needs Input $0.87
* = this machine
Remote sessions also appear in the dashboard (claudectl) and the plain list
(claudectl -l), prefixed with the machine they're running on:
Press Tab on one to attach to it: claudectl sshes to that machine and attaches
to the session's tmux pane — in a new tmux window if the dashboard runs inside
tmux, otherwise in this terminal until you detach (Ctrl-b d). That needs the
session to run under tmux there, ssh access to the machine as the user running
it, and claudectl on both sides new enough to report panes and addresses.
Without them, Tab says which one is missing.
The other keys — approve, send input, terminate — act on local sessions only. To put work on another machine, delegate it:
claudectl relay delegate mac-mini-9f2a1b "run the full test suite" --cwd ~/code/claudectl
claudectl relay status # how delegated tasks are doing
claudectl relay interrupt --peer mac-mini-9f2a1b task_123 stop
Step 6: Move a conversation to another machine
Delegation starts fresh work over there. To continue this conversation on another machine instead, send the session itself:
claudectl relay send-session mac-mini-9f2a1b <session-id>
claudectl relay sessions # what this host sent, and what came back
From the dashboard, press S on the highlighted session. It lists your
paired machines, numbered; a digit sends, Esc cancels:
The hotkey checks what it can immediately — that the transcript exists, that the machine is paired, that there is an address to dial — and then hands the transfer to a background process, because a median transcript is 5.45 MB. So the status line says sending, not sent.
claudectl relay sessions tracks what this host has sent, and reaches
received with the path on the far side and the command that resumes it there
— the receiver answers on the delivery's own connection, so no listener is
needed on the sending side.
A row left at sent means the acknowledgement did not arrive within five
seconds. The transfer may still have landed; the receiver's log says. A peer
running 0.78.0 or earlier never acknowledges, so every row stays at sent
against one of those.
What arrives is the full message history, tool calls and results included. What does not: the working tree and git state, subagent transcripts, running processes, local hooks and environment, and the session id itself — the receiver forks rather than continuing the same id, so two hosts can never diverge on one session. Check out the same commit over there first, or the conversation will resume talking about files that aren't where it left them.
The receiving host prints the command that resumes it. Peers on another OS
need --remote-cwd, since the sender's own path won't exist there.
How the cluster view works
Each relay advertises its own sessions on every heartbeat (default 30s) and
writes what its peers report to ~/.claudectl/relay/fleet.json. The dashboard
and relay fleet read that file, so no port or token is needed for the local
view.
Consequences worth knowing:
- A relay must be running on each machine.
relay fleetwith no snapshot falls back to local sessions and tells you so. On macOS,relay install-agentkeeps one running across logout and reboot; otherwiserelay serveis a foreground process that stops when its terminal does. - A peer that stops reporting disappears after 90s rather than lingering as a stale row, and the whole snapshot is ignored after 120s (which is how a stopped relay shows up as "no snapshot" instead of a frozen cluster).
- Remote rows carry what the heartbeat carried: project, status, cost, tokens, elapsed. Live CPU and memory stay local-only.
The coordinator's HTTP API serves the same unified view to anything that can
speak HTTP. It binds 127.0.0.1 and starts only when both --http-port and
--auth-token are given:
claudectl relay serve --http-port 9876 --auth-token secret
curl -H "Authorization: Bearer secret" http://localhost:9876/api/sessions
Reaching that API from a dashboard on another machine is a second, deliberate step, because the API is plaintext HTTP/1.1 and the bearer token crosses the wire in the clear. Forward the loopback port instead of widening the listener — from the machine running the dashboard:
ssh -L 9876:127.0.0.1:9876 user@machine-a
curl -H "Authorization: Bearer secret" http://localhost:9876/api/sessions
Cloudflare Tunnel and Tailscale Funnel do the same job without SSH. You can
bind the API to every interface with --http-addr 0.0.0.0; it works, it prints
a warning at startup, and Security says why you probably want the
tunnel.
Keeping the background stack alive
relay serve is a foreground process. Close the terminal and the relay stops, which means the cluster view on every peer goes stale — relay fleet falls back to local sessions and says so. The same is true of the half that does the orchestrating: the supervisor reconciler and the brain tick inside claudectl --headless, which is also a foreground process.
Install them as services instead. A launchd agent on macOS, a systemd --user unit on Linux — same commands on both:
claudectl relay install-agent # the relay, port 9847
claudectl relay install-agent --port 9850 --http-port 9876 --auth-token secret
claudectl relay install-agent --headless # the relay and the headless service
claudectl relay install-agent --headless --brain # ...with the local-LLM brain enabled
claudectl relay agent-status
claudectl relay uninstall-agent # removes both
Installed the relay agent.
plist: /Users/you/Library/LaunchAgents/io.claudectl.relay.plist
label: io.claudectl.relay
port: 9847
logs: /Users/you/.claudectl/relay/agent.out.log
/Users/you/.claudectl/relay/agent.err.log
It is running now, starts at login, and restarts if it dies.
The agent starts at login (RunAtLoad) and is restarted if it exits (KeepAlive), so a crash or a kill brings it straight back. claudectl doctor reports whether it is running.
The headless service
--headless installs a second service for the orchestrating half of the stack. It is one service, not two: the supervisor reconciler and the actuator tick in the same process as the brain, so claudectl --headless covers both. (supervisor run is not a daemon — it submits a tasks.toml and exits.)
Two things are deliberately off by default:
--brainenables the local-LLM brain. Without it the service still runs the supervisor reconciler and the coordination loop, which is the half with no other way to stay up. With it, an unreachable endpoint is not fatal: the process warns and runs on without the brain, so it will not crashloop on a machine whose endpoint comes and goes.--auto-runlets the brain act on its own suggestions, and implies--brain. A service installed once at login that executes decisions without being asked is opt-in, so you have to ask for it by name.
Loaded is not working
A service manager restarts a process that exited. Neither launchd nor systemd notices one that is alive and doing nothing — which is what actually went wrong: a relay agent ran happily for days while its only link flapped.
So both services publish a liveness signal, and doctor reads it rather than just asking whether the service is loaded:
| Service | Signal |
|---|---|
| relay | ~/.claudectl/relay/fleet.json, rewritten every tick |
| headless | ~/.claudectl/headless/heartbeat.json, rewritten every tick |
Either going stale for more than 120s is a doctor failure that names the log and the fix:
✗ headless service loaded, but it has stopped ticking — wedged, not running
→ Nothing has written the heartbeat for over 120s. Check
~/.claudectl/headless/agent.err.log and restart with
`claudectl relay install-agent --headless`.
Neither check nags a machine that deliberately installed no service: not-installed is an advisory, not a failure.
Details worth knowing:
- Re-run
install-agentto change anything. It rewrites the service definition and reloads it, so changing a port or picking up abrew upgradeis one command. - Re-running changes nothing when nothing changed. An already-running service whose definition is byte-identical is left strictly alone, so adding
--headlessdoes not restart a relay you did not ask it to touch. - Uninstall never leaves an orphan. The plist is removed even if unloading complains — an orphaned plist that keeps resurrecting a service you thought you removed is the usual way this feature goes wrong.
agent-statusalso detects the reverse case (loaded, plist missing) and tells you to rununinstall-agent. --auth-tokenlands in the plist, under~/Library/LaunchAgents, readable by your user. The install output says so when you pass one.- The relay is killed abruptly on unload, not asked to shut down. That is safe:
fleet.jsonand the knowledge store are both written atomically, so the worst case is losing the current one-second tick rather than a torn file.
Linux
The same commands. install-agent writes a real systemd --user unit — ~/.config/systemd/user/claudectl-relay.service, and claudectl-headless.service with --headless — runs daemon-reload and enable --now, then calls loginctl enable-linger so the unit survives logout the way a launchd agent does.
Lingering needs policy permission. If it is refused, the install still succeeds and prints the command to run by hand:
Logs go to the journal rather than to files, since systemd has one and launchd does not:
Three Ways to Share a Code
Every invite generates three formats. Pick whichever fits the situation:
Relay Code (compact, no IP visible)
15 characters. Speakable over a phone call. Encodes the IP, port, and key without exposing any of them in readable form.
Word Phrase (memorable)
9 common English words. Easier to dictate than alphanumeric codes.
Invite Link + QR Code
Plus a scannable QR code in the terminal (requires qrencode installed).
The join command auto-detects the format:
claudectl relay join YEK-AGA-YHK-QAA-BM # relay code
claudectl relay join fur-hue-ace-bid-ice-..." # word phrase
claudectl relay join cctl://laptop-a3f2@..." # invite link
LAN Discovery
Find nearby claudectl instances without codes:
Found 2 instance(s):
IDENTITY ADDRESS VERSION
────────────────────────────────────────────────────────
laptop-a3f2 192.168.1.50:9847 v0.40.0
ci-runner-9d1e 192.168.1.101:9847 v0.40.0
Discovery is passive: relay serve broadcasts a small announcement on UDP 9848 every 5 seconds, and relay discover listens for 6 seconds — one second longer than the interval, so every announcing peer is heard at least once. discover sends nothing itself.
Turn the broadcast off with:
Before v0.66.0 nothing ever sent an announcement, so
relay discoveralways reported "no instances found". If you tried it on an older build and concluded LAN discovery was broken, it was.
Hive Identity
By default a hive has no name. It is whatever your relay peers happen to be — the transitive closure of who you paired with. Nothing is advertised, nothing is discoverable, and that is the state every install starts in.
Naming one is what makes it possible to advertise, find and join a hive as such:
claudectl hive identity # show it, or "unnamed"
claudectl hive identity set --name barrys-hive \
--description "Rust CLI + Claude Code practices"
claudectl hive identity clear --yes # back to unnamed
Stored at ~/.claudectl/hive/identity.json:
{
"hive_id": "hv_3a9f21",
"name": "barrys-hive",
"description": "Rust CLI + Claude Code practices",
"join_policy": "invite",
"created_ms": 1791210482180
}
Renaming keeps the hive_id and the creation time — a rename is not a new hive, and peers who know it by id keep recognising it. The name must be usable as a capability scope qualifier ([A-Za-z0-9._-]), because a later phase grants hive.read:<name>.
Join policy
| Policy | Meaning |
|---|---|
invite |
A link or code is required. The default. |
ask |
You approve each join request. |
open |
Anyone who can see your LAN broadcast may join, without approval. |
open is allowed, but it asks first:
WARNING: join_policy = open lets ANY machine that can see your LAN broadcast
join this hive without approval. Joining a hive means receiving your distilled
preferences and insights — the patterns the brain learned from how you work. On
a shared or untrusted network (an office, a cafe, a conference) that is everyone
on it.
`ask` gives you the same discoverability and still lets you approve each
request. Prefer it unless you specifically want hands-off joining on a network
you control.
Open this hive to anyone on your LAN? [y/N]
Two details worth knowing:
- Consent is recorded, not just prompted. Confirming writes
open_acknowledged_msinto the record.identity.jsonis an ordinary file you can edit, so a prompt alone would be theatre — hand-editing"join_policy": "open"leaves it not in force, and claudectl treats it asinviteuntil you run the command and confirm.hive identitysays so plainly when that happens. - No terminal means
--yesis required, not assumed. In a script or CI,--join-policy openfails without it. Defaulting to yes in a pipe is how a permissive setting gets made by accident.
Moving away from open drops the acknowledgement, so coming back to it asks again.
Inviting someone into the hive
claudectl hive invite # link + relay code
claudectl hive invite --words # also the memorable phrase
claudectl hive invite --qr # QR of the link
Inviting to hive "barrys-hive" (hv_3a9f21), join_policy=ask
Each join will wait for you to approve it (claudectl hive requests).
HIVE LINK: cctl://hive/hv_3a9f21?a=laptop-a3f2@192.168.1.50:9847&k=cbc3179ea1a8bb70&n=barrys-hive&p=ask
RELAY CODE: YCU-AIG-B7L-VNU-HAS-MFS-BRA
(the code and the phrase pair them with this machine; only
the link names the hive — either way they end up asking to join)
The invite carries the same address and PSK a peer invite does, because the holder still has to reach this machine. Only the link carries the hive id — the relay code and the word phrase spend all thirteen of their bytes on the address and the key. That costs nothing: a code pairs you with the machine, and the machine then answers for whichever hive it runs.
The RFC sketched this link as
cctl://hive/<hive_id>?k=<psk>&n=<name>, which cannot be used — it names a hive but no machine, so a holder has nothing to connect to. The address is carried ina=<identity>@<host:port>, the same pair the peer link already puts before its/k/.
hive invite warns if nothing is listening on the port it is about to hand out,
because an invite minted while the relay is down cannot be redeemed.
Joining a hive
claudectl hive join cctl://hive/hv_3a9f21?a=... # link
claudectl hive join YCU-AIG-B7L-VNU-HAS-MFS-BRA # relay code
claudectl hive join nut-may-aim-bud-era-cow-... # word phrase
All three pair with the machine and then ask to join. What happens next is the
host's join_policy to decide — holding a link is not the same as being let in:
Hive "barrys-hive" (hv_3a9f21), join_policy=ask
Connecting to 192.168.1.50:9847...
Paired with laptop-a3f2 (192.168.1.50:9847)
Asking to join "barrys-hive"...
Asked to join "barrys-hive" — waiting for its owner to approve. Nothing is
shared until they do.
To start exchanging knowledge, connect to the hive:
claudectl relay connect 192.168.1.50:9847
That last line matters: hive join records membership and exits. relay serve
listens but does not dial out, so the joiner is the side that has to connect for
knowledge to actually move.
claudectl hive status always says where you stand:
A link that names a different hive is refused, which is what stops an invite for someone else's hive from quietly joining yours.
Approving who gets in
With join_policy: ask, each request waits for you:
claudectl hive requests # who is waiting, and who is in
claudectl hive requests approve laptop-a3f2
claudectl hive requests deny laptop-a3f2
Hive "barrys-hive" (hv_3a9f21), join_policy=ask
1 waiting for you:
PEER REQUEST ASKED
────────────────────────────────────────────────────────────────────────
laptop-a3f2 jr_1791349617144_0 just now
Approving tells the peer immediately if it is reachable; otherwise it finds out
the next time it connects or re-runs hive join. Denying leaves the peer
paired — a denial is about the hive, not the machine — but it gets nothing from
the hive, and re-asking does not undo it.
hooks.on_hive_join_request fires when a request is queued, so you do not have
to be watching the terminal:
[hooks.on_hive_join_request]
run = "osascript -e 'display notification \"$CLAUDECTL_HIVE_JOIN_PEER wants to join\"'"
Joining as a reader
A reader meshes with the hive and receives its knowledge, and never contributes any — the hive analogue of read-only project access. It is the one membership tier the owner hands out explicitly, with a capability grant:
# the owner, once
claudectl access grant --scopes hive.read --project barrys-hive \
--label "alice, read-only" --expires 30d
claudectl hive invite
Presenting a hive.read grant — asking to join as a reader.
Joined hive "read-hive" as a reader — you will receive its knowledge, and
nothing of yours is sent.
Both halves are needed. The invite is transport authentication — it pairs the
two machines. The grant is the hive-level role, and says what the peer may do
once paired. Neither alone is enough, and --project on access grant names the
hive here, because hive.read:<hive-name> is the scope.
A grant admits directly, even on an ask hive. The owner already decided when
they minted it; queueing the peer would be asking them the same question twice.
So a reader is never in the hive requests queue — it appears straight away in
the members list, with its role:
PEER ROLE HOW ADMITTED
barrys-mac-b0ceaa2b reader grant just now
1 of them are readers: they receive this hive's knowledge and
contribute none of their own.
What a reader may and may not do:
| Reader | Contributor | |
|---|---|---|
| Receive knowledge units | yes | yes |
| Ask for a snapshot | yes | yes |
| Contribute units | no | yes |
Appear in hive requests queue |
no — the grant is the approval | only under ask |
A reader's attempted contribution is refused on the wire, not silently dropped. The host answers with a rejection naming the reason and how many units it discarded, and the reader prints it:
[2026-10-07T09:17:28Z] barrys-mac-fc78cb5c refused our knowledge:
this hive admitted you as a reader — readers receive knowledge but do not
contribute it (2 unit(s) dropped)
Silently ignoring it would be indistinguishable, at the reader, from a network fault.
Readers need nothing from hive trust: TrustTier weighs how much a peer's
claims count for, and a reader never makes any, so merging, drift detection and
concordance checking are unchanged by this tier.
Three limits worth knowing before you hand out a reader grant:
- The roster is authoritative after admission. Revoking the grant does
not demote an existing reader — it only stops new admissions. The grant
id is recorded on the member file so you can see the connection. Removing a
member is
rm ~/.claudectl/hive/members/<peer>.jsonfor now; making revocation reach the roster is a follow-up. - Renaming the hive invalidates every outstanding reader grant, because the
hive name is the scope qualifier.
hive identity set --namewarns and lists the grants that will stop working. - A reader is still a paired peer. Read-only is about the hive, not about the relay: the two machines can still exchange heartbeats and fleet data.
What membership actually gates
Knowledge is exchanged only with hive members, and in both directions. A peer that is merely pending receives no units and cannot contribute any — gating only what you send would let an unapproved peer push into the hive while getting nothing back.
Three things worth knowing:
- An unnamed hive gates nothing. Membership only exists once a hive has a name, so nothing changes for anyone who has not named one.
- Naming a hive admits everyone you had already paired with. They were trusted before the hive had a name, and naming it does not withdraw that — otherwise gossip would stop dead until every peer re-joined. The command says who it admitted. A peer you have denied is not admitted this way.
- A reader receives but never contributes, which is the one case where the two directions differ. See Joining as a reader above.
inviteandopenare the same rule at the host today. The PSK is per-host rather than per-invite, so the host genuinely cannot tell which link a peer used — pairing is the credential. The two policies differ in your intent and in what gets advertised on the LAN, not yet in what the host enforces. Onlyaskcurrently changes who gets in. Per-invite tokens are the follow-up that would makeinviteenforce what its name says.
Membership is stored as one create-only file per peer under
~/.claudectl/hive/members/, so the gossip gate is a single stat and
relay serve admitting a peer cannot collide with hive requests approve
running in another process. A joiner's own standing is in
~/.claudectl/hive/membership.json — one hive per machine.
Finding hives on the LAN
Once a hive has a name, relay serve advertises it alongside the machine's own announcement, and hive discover lists hives rather than machines:
Found 1 hive(s):
HIVE POLICY PEERS UNITS MACHINES
──────────────────────────────────────────────────────────────────
barrys-hive invite 3 412 1
hv_3a9f21
laptop-a3f2@192.168.1.50:9847
Rows are grouped by hive id, so several machines in one hive collect under it. relay discover also grew a HIVE column, showing — for a machine whose hive is unnamed.
Three things to know:
- An unnamed hive advertises nothing. No
hivekey is added to the datagram at all, so an unnamed machine sends exactly what it always sent and does not appear inhive discover— only inrelay discover. - The policy advertised is the effective one. A hand-edited
openthat was never confirmed goes on the wire asinvite. The consent check lives in the stored record, not in the CLI, precisely so this path cannot leak it. - Renaming takes a relay restart. The identity is read once at startup, like the index in
query serve. If you run the launchd agent,relay install-agentagain (orlaunchctl kickstart -k) picks up the new name.
Peer and unit counts are advisory — they are snapshots from whenever the announcer last ticked, at most one interval old.
Joining a hive by link is a later phase; this makes one findable.
Hive Mind: Knowledge Sharing
The hive mind is the layer that makes connected brains smarter.
What propagation actually does today
Both ends of a connection offer each other whatever the other has not seen,
every 12 seconds, plus immediately when the serving side distills something
new. It does not matter which machine dialled: relay serve and relay join
run the same gossip code.
The offer is incremental — each side records what it has already sent a given peer — so a tick with nothing new costs nothing, and one plain interval covers every case that would otherwise need its own trigger: connecting, reconnecting, being approved after a spell in the pending queue, and a unit distilled while the link was down.
Membership gates it at both ends. The host sends only to peers on its roster and merges only from contributors; a peer that joined as a reader does not push its own units upstream. A refusal is sent back on the wire rather than silently dropped, and the refused batch is re-offered once the gate opens, so approving a queued request does not lose the knowledge that was turned away before it.
One limit worth knowing: relay serve does not dial out. It redials a peer
it has lost, but it does not connect to known peers at startup. Somebody has
to dial.
A second connection from the same machine — claudectl hive join opens its own
while relay join holds a durable one — displaces the first, which then closes
and is redialled within about ten seconds. Knowledge flows either way round
(#459); you may see one Peer ... disconnected and Reconnected to ... pair in
the log while that happens.
How it works once it does fire
- Your brain distills patterns every 10 decisions (e.g., "approve
cargo testat 95% confidence") - These patterns become knowledge units stored in
~/.claudectl/hive/knowledge.jsonl - When connected to peers, knowledge units sync via gossip protocol — new units are sent to all peers
- Incoming knowledge is merged using conflict resolution — your local preferences always win
- Peer knowledge appears in the brain prompt with trust labels
Trust tiers
Each peer has a trust level (0.0 to 1.0) that determines how their knowledge appears in the brain prompt:
| Trust | Tier | Label in prompt | Meaning |
|---|---|---|---|
| >= 0.8 | Confirmed | [hive] |
High confidence, treated as reliable |
| >= 0.5 | Suggested | [hive, suggested] |
Default for new peers |
| >= 0.2 | Unverified | [hive, unverified] |
Low confidence, informational only |
| < 0.2 | Ignored | Not shown | Knowledge excluded from prompts |
Trust adjusts automatically: when your brain makes a decision that agrees with hive knowledge, the source peer's trust drifts up (+0.01). Disagree, it drifts down (-0.01).
View and manage knowledge
# Overview
claudectl hive status
# List all knowledge units
claudectl hive knowledge
# Filter by source peer
claudectl hive knowledge --from ci-runner
# Filter by scope
claudectl hive knowledge --scope project:myapp
# Export all knowledge as JSON
claudectl hive export > team-knowledge.json
# Import knowledge from a file
claudectl hive import team-knowledge.json
# Remove a specific unit
claudectl hive forget ku_1745539200_3
Manage trust
# Show all peer trust levels
claudectl hive trust
# Show trust for one peer
claudectl hive trust ci-runner
# Manually set trust
claudectl hive trust ci-runner 0.9
Remote Task Delegation
Delegate orchestrator tasks to connected peers. The remote machine spawns its own Claude Code session and reports status back.
Task file with peer routing
{
"tasks": [
{
"name": "fix-tests",
"prompt": "Fix the failing auth tests",
"cwd": "/path/to/project",
"peer": "ci-runner-9d1e"
},
{
"name": "update-docs",
"prompt": "Update the API docs",
"cwd": "/path/to/project"
}
]
}
Tasks with "peer" are delegated to the remote machine. Tasks without "peer" run locally. Dependencies work across local and remote tasks.
Manual delegation
Interrupts
# Nudge a remote task (informational)
claudectl relay interrupt task_123 nudge 'dependency resolved'
# Stop a remote task
claudectl relay interrupt task_123 stop 'no longer needed'
TUI Integration
Peers panel
Press p in the TUI to toggle the peers panel:
┌─ Peers (2) ──────────────────────────────────────────────┐
│ ● laptop-a3f2 connected trust:0.8 ↑12 ↓8 kb │
│ ● ci-runner-9d1e connected trust:0.5 ↑42 ↓0 kb │
└──────────────────────────────────────────────────────────┘
Brain prompt integration
When the brain evaluates a session, hive knowledge appears as a separate section:
## Hive Knowledge (2 peers, 15 units)
- [hive] [Bash, cargo test] approve (95%) — 20 decisions from laptop-a3f2
- [hive, suggested] [Write, *.lock] deny (88%) — 12 decisions from ci-runner
Configuration
Add to .claudectl.toml or ~/.config/claudectl/config.toml:
[relay]
enabled = true # start relay with TUI/brain
listen_port = 9847 # TCP port for peer connections
listen_addr = "0.0.0.0" # bind address — peer transport only, not the HTTP API
max_peers = 8 # maximum connected peers
heartbeat_interval_secs = 30 # heartbeat frequency
reconnect_max_secs = 60 # max reconnect backoff
auto_connect = [] # list of "host:port" to auto-connect
http_addr = "127.0.0.1" # bind address for the coordinator HTTP API.
# Deliberately loopback, and deliberately
# separate from listen_addr.
# http_port = 9876 # no default; unset means no HTTP API at all
# auth_token = "…" # no default; the API's bearer token
[hive]
enabled = true # enable knowledge sharing
default_trust = 0.5 # trust level for new peers
auto_trust_drift = true # adjust trust based on concordance
max_propagation = 5 # max gossip hops for knowledge units
export_min_evidence = 5 # min decisions before sharing a pattern
export_min_tool_decisions = 10 # min decisions before sharing accuracy
knowledge_ttl_days = 30 # expire unvalidated knowledge after N days
inject_unverified = true # include low-trust knowledge in brain prompt
max_units = 500 # hard cap on stored knowledge units
max_prompt_units = 20 # cap on units injected into brain prompt
stale_peer_days = 90 # prune knowledge from peers gone this long
share_categories = [] # empty = share all (or: ["best_practice", "technique"])
exclude_tools = [] # tools to never share (e.g., ["Write"])
exclude_commands = [] # command patterns to never share
http_port and auth_token have no defaults, and the HTTP API starts only
when both resolve — leave either unset and there is no listener. The bind
address is resolved --http-addr first, then http_addr, then 127.0.0.1.
CLI Reference
Relay commands
| Command | Description |
|---|---|
relay serve [--port N] |
Start the relay listener |
relay serve --http-port N --auth-token T [--http-addr ADDR] |
Also start the coordinator HTTP API. --http-addr defaults to 127.0.0.1 |
relay install-agent [--port N] [--http-port N] [--http-addr ADDR] [--auth-token T] |
macOS: keep the relay alive across logout and reboot |
relay agent-status |
Whether the launchd agent is installed and running |
relay uninstall-agent |
Remove the launchd agent |
relay invite [--qr] [--words] |
Generate invite code/link/phrase |
relay join <code> |
Join using any invite format |
relay discover |
Scan LAN for nearby instances |
hive discover |
Scan LAN for named hives, grouped by hive |
relay pair |
Generate a raw PSK code |
relay accept <code> <peer> |
Accept a raw PSK from a peer |
relay connect <host:port> |
Connect to a remote relay |
relay peers |
List paired peers, live first, with status (live, stale, never) and when each was last heard |
relay forget <peer> |
Remove a peer |
relay prune [--older-than 7d] [--yes] |
List paired peers quiet for longer than that; --yes forgets them. Keeps any peer your hive depends on. Needs this machine's relay running, since it is what records heartbeats |
relay identity |
Show this instance's relay identity |
relay delegate <peer> <prompt> |
Delegate a task |
relay status |
Show remote task status |
relay interrupt <task> <type> |
Interrupt a remote task |
Hive commands
| Command | Description |
|---|---|
hive status |
Show knowledge store overview |
hive knowledge [--from X] [--scope Y] |
List knowledge units |
hive export |
Export knowledge as JSON |
hive import <file> |
Import knowledge from JSON |
hive forget <unit-id> |
Remove a knowledge unit |
hive trust [<peer> [<level>]] |
Show/set peer trust levels |
hive archive [--prune Nd] |
Show cold storage archive stats |
hive distill |
Run distillation pipeline on archive |
hive curriculum |
Show distilled curriculum |
Architecture
┌──────────────────────────────────────────────┐
│ HIVE MIND │
│ distill → knowledge units → gossip → merge │
├──────────────────────────────────────────────┤
│ REMOTE DELEGATION │
│ delegate → remote spawn → status → handoff │
├──────────────────────────────────────────────┤
│ RELAY │
│ TCP + PSK auth + NDJSON + heartbeats │
└──────────────────────────────────────────────┘
- Relay: TCP transport with HMAC-SHA256 pre-shared key authentication, NDJSON wire protocol, heartbeats with exponential backoff reconnect
- Delegation: Remote task execution with periodic status updates, handoffs, and interrupt support
- Hive Mind: Gossip-based knowledge sharing with conflict resolution (local always wins), trust-weighted brain injection, epidemic propagation with TTL. See what propagation actually does today for the sync interval and the two limits that remain
Security
The relay runs two listeners whose authentication and defaults have nothing in common, so each claim below is scoped to one of them.
Peer transport (TCP, listen_addr:listen_port, default 0.0.0.0:9847)
- Every connection is authenticated via HMAC-SHA256 challenge-response, and the
handshake proof is compared in constant time (
relay::crypto::ct_eq, #426) - PSK pairing requires explicit action on both sides
- Auth rate limiting: 5 failed attempts = 60s cooldown per IP
- Max concurrent auth threads capped at 16
- Session state and knowledge go only to paired peers (LAN discovery still broadcasts identity, port and version to the local segment)
- For encryption, tunnel through SSH or WireGuard
- Knowledge never overrides local preferences (deny-first)
Non-loopback is this listener's intended deployment, which is why it still
defaults to 0.0.0.0.
Coordinator HTTP API (http_addr:http_port, default loopback)
The bullets above describe the peer transport and do not hold here. This
listener authenticates with the single --auth-token bearer token, compared in
constant time as of #426. It has none of the rate limiting or connection caps
the peer listener has, and the token carries no scope and no revocation short
of restarting with a different one. A leaked token is full access to the API —
every session on the cluster, plus the POST /api/heartbeat write — until the
relay restarts.
Transport
426 settles Q3 in the open-cluster RFC: the
HTTP API binds 127.0.0.1 by default and off-machine access is the
operator's tunnel to arrange — Cloudflare Tunnel, Tailscale Funnel, or
ssh -R. There is no TLS anywhere in this path. The API speaks plaintext
HTTP/1.1, so on any hop that is not tunnelled the bearer token and the session
data both travel in the clear.
rustls was the alternative, and it was rejected on the dependency rule: the
sync core runs on 7 runtime crates and Cargo.toml carries no TLS crate at
all. A tunnel covers the same boundary without one. The cost is operator setup,
and fronting the API does move session data off your network — that is what a
dashboard on another machine means, tunnel or not.
Before #426 the HTTP API inherited relay.listen_addr, so
claudectl relay serve --http-port 9876 --auth-token secret put the plaintext
API on every interface. It binds loopback now. Pass --http-addr 0.0.0.0 (or
set http_addr) for the old behavior; listen_addr no longer governs the HTTP
API either way.
Both HTTP listeners warn at startup when the address they bind is not loopback
— 0.0.0.0, ::, or a specific LAN IP. That covers the coordinator API and
claudectl supervisor metrics, whose /metrics endpoint has no authentication
at all and defaults to 127.0.0.1:9464.
FAQ
Do I need to build with --features relay?
No. relay has been in the default feature set for a while — cargo install claudectl and the Homebrew bottle both ship it. The feature still exists for the minimal sync-only build (--no-default-features --features hive), which drops the networking and leaves hive local.
Does it work across different networks? Yes, if the machines can reach each other over TCP (port 9847). For machines behind NAT, use a VPN like Tailscale or WireGuard, or SSH port forwarding.
What happens if a peer goes offline? The connection drops, heartbeats detect it within 90 seconds, and the initiating side reconnects with exponential backoff. Knowledge already synced persists locally.
Can a malicious peer poison my brain? No. Local knowledge always wins. Peer knowledge is labeled with trust tiers and never overrides your own preferences. Low-trust peers' knowledge can be excluded entirely.
How much data is transferred? Knowledge units are small JSON records (100-300 bytes each). A typical sync between two peers transfers a few KB. Snapshots for new peers are paginated at 500KB.