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 0600HADRON_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 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_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).