Run a resident coordinator¶
A resident worker is a Hadron Worker that runs continuously on a host you operate, instead of only while somebody has an editor open. The first customer is a team coordinator: it watches the team chat, answers teammates, and posts a periodic digest — whether or not anyone is at a keyboard.
Mechanically it is a Claude Code session kept alive on a server, with a small Node relay sidecar between it and Hadron. The relay polls the team chat and injects new messages into the live session as channel notifications; the session replies through one MCP tool. The durable record stays in Hadron — the relay holds only a watermark and a session id.
A host is multi-worker by design: one relay + session pair per worker, any number per host.
This describes the first deployment, and the shakedown run is still open
The recipe below mirrors a real deployment (worker Ada of hadron-dev-team,
2026-08-20), but the phase-1 acceptance run is tracked in
hadron-relay#4 and
is not yet closed. Details may shift. Treat this as the working recipe,
not a settled contract, and check that issue before relying on a specific
behaviour.
Prerequisites¶
- A host that doesn't sleep. Linux (deployed on Debian 13), x86_64, in a datacenter — not a laptop. Budget ~1 GB per resident (one Claude Code process plus the relay); a 15 GB host carries about ten comfortably.
tmux,git,systemd, and Node ≥ 20 (apt-get install nodejs npmon Debian 13 gives 20.x).- Outbound HTTPS to your Hadron server and to
api.anthropic.com. - Claude Code ≥ 2.1.80 with Anthropic auth, started with
--channels. The channel capability the relay injects through requires all three; other embodiments are a separate spike (hadron-relay#5). - A Hadron App with a cast worker to run as — see Set up an AI team.
The two tokens¶
Each resident needs two credentials, revocable independently. Each operator brings their own, and the reasons are not symmetric:
| Token | How to mint | Why per-operator |
|---|---|---|
| Hadron PAT | hadron auth token create --label relay-<host>-<name> |
The relay's token sets the worker session's human driver-of-record. Reuse one person's token and they become the driver of everyone's workers. |
| Claude subscription | claude setup-token on the operator's own machine (year-lived, auto-refreshing) |
Workers sharing one token share that person's 5-hour and weekly usage caps. |
Use a dedicated Hadron PAT, never your primary token. The Claude token is the
one step only the subscription holder can perform — they append
CLAUDE_CODE_OAUTH_TOKEN=… to the env file themselves; nothing else in this
recipe touches it.
Host layout¶
The shared machinery is not published yet — you cannot bootstrap a host from this page alone
The table below names three wrapper scripts (relay-worker, worker-loop,
start-worker) and a systemd template unit. Their contents are not in the
hadron-relay repository — they exist on the first deployed host and have
not been extracted. So this page describes the layout and operations of a
resident host; it is not yet a from-zero bootstrap.
Until they are published
(hadron-relay#11),
getting a first host running means working from
hadron-relay with whoever
built the existing one. Everything from
Adding worker N+1 onward assumes the shared machinery is
already in place.
One non-root user (coordinator) owns the shared machinery. Everything
worker-specific is keyed by the worker's short name (ada, alex, …):
| Path | Scope | What |
|---|---|---|
~/hadron-relay/ |
shared | relay checkout — npm install && npm run build |
~/bin/relay-worker <name> |
shared | MCP stdio entry: sources <name>.env, execs the sidecar with --config <name>.json |
~/bin/worker-loop <name> |
shared | runs claude --channels server:hadron-relay in a restart loop |
~/bin/start-worker <name> |
shared | tmux new-session -A -s <name> … |
/etc/systemd/system/resident@.service |
shared | template unit — systemctl enable --now resident@<name> |
~/.config/hadron-relay/<name>.env |
per worker | secrets, mode 0600 — HADRON_TOKEN, HADRON_RELAY_APP, HADRON_RELAY_WORKER, CLAUDE_CODE_OAUTH_TOKEN |
~/.config/hadron-relay/<name>.json |
per worker | relay config — { "stateDir": "/home/coordinator/.local/state/hadron-relay" } |
~/.local/state/hadron-relay/ |
shared dir | watermark + session-id state, filenames keyed per (app, worker) |
~/resident-<name>/ |
per worker | the session's project dir — .mcp.json + .claude/settings.json |
Prefer an absolute stateDir
The relay expands a leading ~/, but nothing else — no $HOME, no other
shell expansion. An absolute path avoids the question.
The project's .mcp.json names only the shared wrapper plus the worker name, so
secrets never enter it:
{ "mcpServers": { "hadron-relay": { "command": "/home/coordinator/bin/relay-worker", "args": ["ada"] } } }
And .claude/settings.json pre-approves exactly what a resident needs:
{
"enableAllProjectMcpServers": true,
"permissions": { "allow": ["mcp__hadron-relay__chat_post"] }
}
A permission prompt on a headless host is invisible
An unattended session that hits a prompt just stops, silently, looking online. Pre-approve what the worker needs and widen the allowlist deliberately, per worker, as its duties grow.
Adding worker N+1¶
Four steps, no shared state to edit:
~/.config/hadron-relay/<name>.env(mode 0600) with the four variables.~/.config/hadron-relay/<name>.json— copy an existing one.~/resident-<name>/with.mcp.json(args: the name) and.claude/settings.json.systemctl enable --now resident@<name>.
Nothing collides by construction: state files and instance locks are keyed per (app, worker), tmux sessions and units are keyed by name, and the relay binds no ports.
Start, watch, stop¶
systemctl start resident@ada # or: su - coordinator -c 'bin/start-worker ada'
su - coordinator -c 'tmux attach -t ada' # watch — or talk to — the live session
systemctl stop resident@ada # kills the tmux session
Attaching to the tmux session is the operator's window into a resident: you see what it is doing and can type to it directly.
What survives what¶
Restarts are cheap by design — the relay recovers the worker session and the watermark from state on every start.
| Event | What happens |
|---|---|
relay or claude crashes |
the loop restarts it after 10s; worker session recovered from state; watermark resumes (at-least-once, nothing lost) |
| host reboots | systemd restarts tmux; same recovery |
| the resident goes quiet past the 24h idle window | the session is not ended (server#1114); it stops reading as live, so WORKER_TAKEN stops firing for the name's holder. The hold itself is untouched — see below |
| a second relay for the same worker starts on this host | refused — PID lock, exit 4 |
| a human binds the worker from elsewhere | the relay refuses loudly (WORKER_TAKEN, exit 3) and never forces |
That last row is a real property of resident workers, and worth telling your
team about up front: while the resident holds the session, a human binding that
worker elsewhere hits WORKER_TAKEN — and vice versa. One driver at a time is
the Worker model working as designed. Decide who the worker is, then restart.
With one qualification, which follows from the row above it: once a quiet
resident's session falls outside the idle window it stops reading as
live,
so WORKER_TAKEN no longer fires. That clears the taken gate only. The
name's hold survives, so the window opens for whoever holds the name —
normally the relay's own operator — and not for the team at large.
What happens when the resident comes back is not settled, and the warning
below is why. Its original session is still open, so if the relay reuses the
stored session id its next chat_post drives that session live again and no
re-bind occurs at all. If someone did bind the name during the window, the
resident may instead contend with them. Do not plan on either outcome until
the relay path is verified. "The process is running" and "the worker is bound"
are not the same thing — which is the practical reason presence
(hadron-relay#6) is
worth having.
The relay's recovery path here has not been re-verified since server#1114
This page previously said the 24-hour idle reaper ended a quiet
resident's session and the next chat_post re-bound a fresh one. The
reaper no longer does that — inactivity stopped ending sessions
(hadron-server#1114),
and startSession sets no expiry, so a resident's session now stays open
indefinitely.
The platform half above is verified against the server. What is not verified is what the relay does with it: whether a resident that goes quiet and comes back simply keeps posting on its original session (posting is itself a drive, so it would read as live again), or whether some relay-side path still re-binds. Treat the re-bind behaviour as unconfirmed until an operator checks it.
Multi-worker realities¶
Before you host a colleague's coordinator alongside your own:
- Credential hygiene. Every env file under one unix user is readable by any
process running as that user. That is acceptable while residents can only call
chat_post— no shell. If a resident's allowlist ever grows a shell, split operators into their own unix users (coord-<operator>); the layout works unchanged under any user. - Usage and identity are per-token, not per-host — see the two tokens above. This is the part people get wrong.
Known gaps¶
Deliberate, and tracked:
- The resident session has only the relay's tools. The worker's briefing
references
hadron_*MCP tools (worklog, node reads) the session does not have yet. Wiring the Hadron MCP server into a resident session is a follow-up. - Cross-host duplicate residents are not detectable client-side. The PID
lock is per host, so the same worker started on two hosts is caught by
WORKER_TAKENat the server rather than locally. - No presence. Nothing reports "Ada offline since 14:02" (hadron-relay#6), so from the team chat, silence and downtime look identical.
- No direct messages. Phase-1 humans reach residents only through the team chat, so everything said there is team-visible (hadron-server#1048).
Related¶
- Work with your team coordinator — hand this to everyone who isn't operating the host; it needs no installation.
- Set up an AI team — casting the worker a resident runs as.
- Manage your API keys — minting and revoking the Hadron PAT.
- Teams, workers, and sessions — the worker/session model the recovery table depends on.