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.

Updated Jul 30, 2026

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

Because every machine dials out to the broker, hcom works even on machines that accept no inbound connections — for example, a lab or campus host that is reachable only through a VPN. It joins the bus and coordinates like any other, with nothing exposed inbound. That is what makes otherwise-awkward machines first-class members of the set.

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

Keep these straight and never put them anywhere public: the broker password controls who may connect to the broker at all; the hcom group key / connect token lets a machine read the encrypted bus; and each machine’s amux dashboard token is the password to its dashboard and remote API. They protect different things — losing one is not the same as losing another.

Where to go next

Source: content/guides/connect-machines.md · maintained in the nuilab-agenticai repository.