Docs / Guides / Connect machines with hcom
Guides
Connect machines with hcom
Put a private network under your machines, run one MQTT broker, join each machine to an encrypted agent bus, and drive every machine's amux from one hub.
Once you can run agents on one machine , connecting more is mostly networking. You add three things: a private network the machines share, one broker they all talk through, and the hcom bus on each of them. After that, agents on different machines can coordinate — claim a shared task, warn that two of them are about to edit the same file — and you can drive any machine’s amux from a single hub.
Step 1 — a private network
Put the machines on a private mesh so they can reach each other without being exposed publicly. Tailscale does this with little setup: install it on each machine, sign in, and each one gets a stable private address. Everything below — the broker, the bus, the dashboards — binds to those private addresses, so nothing is on the open internet.
Step 2 — one broker
The bus syncs through an MQTT broker, so run one mosquitto instance on a machine the others can reach (a small always-on box is a good choice). Bind it to its private address and give it a password, as that page describes. You only need one broker for the whole set.
Step 3 — hcom on each machine
Install hcom everywhere (it builds with Rust):
cargo install --git https://github.com/aannoo/hcom
Create the relay group once, on any machine, pointing it at your broker. That prints a connect token. Every other machine joins with that token and the broker password, then turns the bus on.
# once, on the first machine
hcom relay new --broker mqtt://<broker-private-IP>:1883 --password '<broker-password>'
# -> prints: hcom relay connect <TOKEN>
# on each machine (including the first), join and start
hcom relay connect <TOKEN> --password '<broker-password>'
hcom relay on
hcom relay daemon start
Run hcom relay daemon start from a service manager so a machine rejoins after a reboot rather than
going quiet until you notice.
Locked-down machines still work
Step 4 — drive other machines from a hub
amux ships amux-remote, which controls another machine’s amux over its dashboard API. Give each
machine a config named after it (below), then name that box first — you can list, attach, peek at,
and steer its sessions from your hub:
amux-remote <machine> ls # list that box's sessions (header names it; -c = compact grid)
amux-remote <machine> peek <name> # see recent output without attaching
amux-remote <machine> attach <name> # attach over SSH
amux-remote <machine> exec <name> --dir <dir> --provider <p> # register+start there, any provider
amux-remote <machine> provider <name> [<p>] # show or change its agent
The provider choice
mirrors exactly what you
would run locally, so a hub can start claude, codex, gemini, or opencode (local-model)
sessions on any machine.
A common setup is to make your laptop the hub: it holds amux-remote configs for each always-on
machine, so you start and check on agents everywhere from one place, while the heavy, long-running
work stays on the machines that are always up. The laptop is the cockpit; the servers are the farm.
Configure each remote
Give each remote machine its own small config file. The amux-config command (from
amux-tools
) writes and maintains them, fetching the
token so it never passes through your terminal, your shell history, or ps:
amux-config add <machine> --host <machine> # reads the token over SSH (--from-ssh, the default)
amux-config test <machine> # prove it answers and the token works
No SSH from here to that machine? --token-stdin reads the token from a pipe instead. Either way
the result is one mode-600 file per machine — never paste the token into a script or commit it:
# ~/.amux/remotes/<machine>.env (mode 600 — one file per remote machine)
AMUX_URL=https://<machine>:8822 # that machine's dashboard URL
AMUX_SSH_HOST=<machine> # host to ride for `attach`
AMUX_TOKEN=<that machine's dashboard token> # keep this file 600; never commit it
amux-remote takes the box name as its first argument — it reads that box’s
~/.amux/remotes/<machine>.env and drives it. No wrapper needed; a bare amux-remote lists the
configured boxes.
amux-remote # list the configured boxes
amux-remote <machine> ls # that box's sessions (add -c for a compact grid)
amux-remote <machine> attach <name>
amux-remote <machine> peek <name>
amux-remote <machine> send <name> "do X"
amux-up — start if needed, then attach
An interactive amux-remote <machine> start now attaches by itself (non-TTY runs — scripts,
pipes — start only, and --no-attach forces that). This wrapper is still worth having: it picks
the right lane for you, local or remote. It takes the machine
first, same word order as amux-remote, and with one argument it resolves locally — so
amux-up <name> (local) and amux-up <machine> <name> (remote) both land you inside the session.
Extra flags ride the remote start (for claude sessions resume is automatic, so --continue is
rarely needed), and a failed start aborts instead of attaching to nothing:
#!/usr/bin/env bash
# amux-up [MACHINE] NAME [flags] — ensure NAME is started, then attach (local or remote).
set -uo pipefail
first="${1:?usage: amux-up [MACHINE] NAME [flags]}"; shift || true
box=""; name="$first"
if [ -f "$HOME/.amux/remotes/$first.env" ] && [ -n "${1:-}" ] && [ "${1#-}" = "$1" ]; then
box="$first"; name="$1"; shift # machine named first, like amux-remote
fi
if [ -z "$box" ] && amux ls -c 2>/dev/null | grep -qE "(^|[[:space:]])$name([[:space:]]|$)"; then
exec amux start "$name" "$@" # local: start also attaches (or reattaches)
fi
[ -n "$box" ] || { echo "not a local session — name the machine: amux-up MACHINE $name" >&2; exit 2; }
amux-remote "$box" start "$name" "$@" || exit 1 # flags ride the start; a failed start stops here
exec amux-remote "$box" attach "$name" # remote: start, THEN attach — on that machine
amux-all — one view across every machine
Once agents run on several machines you want one list instead of querying each by hand. amux-all
prints the local sessions, then each configured remote’s — a compact grid per machine so it reads
as one view — and marks any machine that does not answer instead of hanging on it:
#!/usr/bin/env bash
# amux-all — sessions across the local machine + every configured remote (compact grid per box).
set -uo pipefail
run_to(){ local s=$1; shift; perl -e 'my $t=shift; alarm $t; exec @ARGV or exit 127' "$s" "$@"; }
hr(){ printf '%s\n' "----------------------------------------"; }
hr; echo " $(hostname -s) (local)"; hr
amux ls -c 2>/dev/null || echo " [no local amux]"
for f in "$HOME"/.amux/remotes/*.env; do # one config per remote machine
[ -e "$f" ] || continue
box=$(basename "$f" .env)
# -c: compact grid, matching the local view. --bare: drop the per-command header,
# since we print our own "$box (remote)" line below.
out=$(run_to 15 amux-remote "$box" ls -c --bare 2>&1); rc=$?
hr; echo " $box (remote)"; hr
if [ "$rc" -ne 0 ] || [ -z "$out" ] || echo "$out" | grep -qiE 'refused|timed out|could not'; then
echo " [unreachable]"
else
echo "$out" | sed 's/^/ /'
fi
done
Install any of these the same way as the earlier scripts — chmod +x, then move into ~/.local/bin.
Ready-made versions of amux-up, amux-all, and the ccjump fuzzy picker ship in
amux-nui-public
, so you can install the repo instead of pasting these.
Three secrets, kept apart
Where to go next
- Put a local model on the routine work: Local and frontier models .
- The pieces in detail: hcom , mosquitto , Tailscale .
Source: content/guides/connect-machines.md · maintained in the nuilab-agenticai repository.