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 24h-idle reaper ends the worker session | the next chat_post re-binds fresh and retries once; the receipt tells the model its new session id |
| 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 exception, which follows from the row above it: after the idle reaper
ends a quiet resident's session, the worker is unbound until the next
chat_post re-binds it. A teammate who binds in that window succeeds, and the
resident then hits WORKER_TAKEN on its next reply and goes quiet. "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.
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.