Docs · Reference
Coordination
How separate Claude Code instances coordinate: a per-repo SQLite bus for truth, a one-line-per-event notify file for wakes, and a persistent Monitor that turns an idle instance into an event-driven one.
The bus
Every repo gets exactly one bus: a shared SQLite database (WAL mode) at ~/.claude/coordination/<repo>-<hash>/hands.db, scoped by the repo’s git common directory — which all worktrees share. That’s the trick that makes stations join automatically: a station’s worktree resolves to the same common dir as the expo’s checkout, so it lands on the same bus with no configuration. Two different projects on one machine never share a bus.
Identity is derived per-directory at runtime: the main worktree resolves to expo; managed worktrees resolve to station-<n> (provisioned stations also carry HANDS_ID in their environment). Every message, ticket, and cursor is keyed by these ids.
The problem the wake system solves
“MCP cannot wake an idle interactive Claude Code — the model must choose to call hands_receive.”An MCP server can answer tool calls, but it can’t start a turn. Polling would burn a model turn per poll. Hands closes the gap with a file and a Monitor:
- —Each agent has a notify file:
<coordinationDir>/<id>.notify. A waking event appends one line —ISO-timestamp⇥from⇥subject— nothing more. The message itself is committed to the database before the notify line, so by the time an agent wakes, the work is already receivable. - —Each agent arms a persistent Claude Code Monitor tailing that file:
Monitor({
command: "mkdir -p <coordinationDir> && touch <notify> && exec tail -F -n0 <notify>",
description: "hands inbox — <id>",
persistent: true,
})-n0 ignores the backlog so arming never self-triggers; -F survives rotation. When a line lands, the Monitor fires, the instance wakes, drains its inbox with hands_receive, works, and yields. Between events it sits parked at zero cost. The notify file is a best-effort side channel — “delivery correctness lives entirely in the DB.”
Wake economics
A wake costs one full model turn over the recipient’s entire accumulated context, so the server is engineered to spend them precisely:
- —Non-waking sends.
hands_send({ wake: false })writes the message and skips the notify file entirely — it’s delivered on the recipient’s next natural drain, and never counted as a wake. - —Burst suppression. A recipient with an outstanding, undrained wake isn’t re-notified — one drain returns everything. The pending flag is cleared before reading, so a racing send “can at worst cause one redundant wake — never a lost one.”
- —Accounting. Every real wake is logged and pruned at 24h — the dashboard’s
wakesLastHour/wakes24hper station. - —Change detection.
hands_boardreturns astateHashover every peer’s state, branch, and focus plus active tickets; the expo’s ~15-minute utilization review short-circuits when it hasn’t changed.
Strict pass discipline
Under the default topology: "strict-hub", the server rejects — before any database write or notify — a station broadcasting and a station messaging another station. A blocked send never wakes anyone. The result is a hub-and-spoke topology in which the expo is the only fan-out point, which is both the discipline and the economics. topology: "open" opts out of those two rejections.
The third rejection is not topology-scoped: hands_delegate refuses any caller that isn’t the expo or the sous, in every topology, open included — and it holds the two to different halves of the rail: the sous composes (no to; the ticket lands queued) and the expo assigns (to required). Messaging rules are about who may talk to whom; this one is about who may create work, which the principal set as an absolute rule. A station proposes work upward (hands_ask, or hands_send({to:'expo'})) and the expo fires the ticket.
The passive layer: hooks
Three Claude Code hooks give the bus ambient awareness with zero effort:
- —Stop → publish (async, after every turn): heartbeats presence, records branch and changed files, harvests new commits and memory files into the journal, and rides the books’ debounced push.
- —SubagentStop → subagent-stop (async, when an Agent-tool call finishes): reads that sub-agent’s own bounded transcript, sums its output tokens (deduplicated by message id) and records one usage sample against the pane that spawned it, tagged with the agent type from the call’s
.meta.jsonsidecar. Without it, sub-agent spend is only visible to the dashboard’s periodic transcript scan. Best-effort: an unreadable transcript records nothing rather than failing the hook. - —UserPromptSubmit → board (before every prompt): injects a compact delta — new journal entries, collisions with an online peer, waiting inbox messages, answered escalations, freshly assigned or returned tickets — and stays completely silent when nothing changed.
Across humans: the books as the passthrough
The bus is per-machine, per-repo. Coordination between people never touches it: each kitchen writes its events to the books and reads every other kitchen’s activity back out of them. No expo talks to another directly. The shape has not changed since the books were a git repo — what changed is that kitchens now point at the same database rather than the same remote.
On one machine that works with nothing configured: the books are machine-wide, so a second kitchen on the same laptop is already in the same table. Across machines it is account-gated. hands relay runs on each machine and its books beat is push-then-pull — push first, so a machine sees its own work reflected in the same tick, then pull everyone else’s events down. Without a sign-in nothing leaves the laptop; local-only is the default and the entire free tier.
What travels is events, not files. The relay carries the journal; it does not carry your day pages, role pages, or recipes. Those are written and rendered on the machine that owns them and stay there. Another kitchen’s recent activity reaches you as summarized events read out of your own books, never as their files.