Skip to content

Run a resident coordinator

CLI onlyAdvanced~45 min

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 npm on 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:

  1. ~/.config/hadron-relay/<name>.env (mode 0600) with the four variables.
  2. ~/.config/hadron-relay/<name>.json — copy an existing one.
  3. ~/resident-<name>/ with .mcp.json (args: the name) and .claude/settings.json.
  4. 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_TAKEN at 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).