Living document. Update this, don't create new versions.
AgentWire can manage AI agent sessions on remote machines via SSH. This guide covers adding, removing, and configuring remote machines.
agentwire machine add <id> --host <host> --user <user> --projects-dir <path>Example:
agentwire machine add gpu-server --host 192.168.1.50 --user ubuntu --projects-dir ~/projectsMachine registration is CLI-only today — the portal sidebar's Machines section is read-only. It lists machines (with a live SSH-reachability status dot) and lets you spawn a new session on one, but has no add/remove controls.
agentwire machine remove <id>This:
- Removes from
machines.json - Prints manual cleanup reminders (SSH config entry, GitHub deploy keys, remote VM/user teardown, portal restart)
# List all machines with connection status
agentwire machine list
# Add a machine
agentwire machine add <id> --host <host> --user <user> --projects-dir <path>
# Remove a machine (portal-side cleanup)
agentwire machine remove <id>Registered machines (2):
gpu-server
Host: 192.168.1.50
Projects: ~/projects
Status: ✓ tunnel
do-2
Host: 167.99.123.45
Projects: ~/projects
Status: ✗ no tunnel
Status reflects whether an autossh process for that machine is currently
running (pgrep -f "autossh.*<machine_id>"), not a reachability ping.
All session commands support the session@machine format:
# Create session on remote machine
agentwire new -s myproject@gpu-server
# Create worktree session on remote
agentwire new -s myproject/feature@gpu-server
# Send prompt to remote session
agentwire send -s myproject@gpu-server "run the tests"
# Read output from remote session
agentwire output -s myproject@gpu-server -n 100
# Kill remote session
agentwire kill -s myproject@gpu-server
# List all sessions (includes remote)
agentwire list| Resource | Minimum | Recommended |
|---|---|---|
| RAM | 1GB | 2GB+ |
| Storage | 10GB | 20GB+ |
| CPU | 1 vCPU | 2+ vCPU |
Note: The LLM runs on Anthropic's servers - remote machines only need resources for Node.js and file operations. No GPU required for Claude Code sessions (GPU only needed for TTS with Chatterbox).
AgentWire uses your existing SSH configuration. Ensure you can connect:
ssh <machine-id> # Should connect without password promptFor passwordless access, add your SSH key:
ssh-copy-id user@hostOr add to ~/.ssh/config:
Host gpu-server
HostName 192.168.1.50
User ubuntu
IdentityFile ~/.ssh/id_ed25519
SSH ControlMaster connection multiplexing (built in). AgentWire applies
ControlMaster flags to every ssh it spawns (see agentwire/ssh.py —
ssh_base_opts()), so the first remote op to a host opens a master connection
and parks its socket under ~/.ssh/sockets/; subsequent ops ride that socket
and skip the handshake (~90–140ms saved on loopback, 300–500ms over a VPN). The
socket dir is created on demand; if it can't be created the flags are dropped
and ssh just re-handshakes (no command is lost). Nothing to configure.
To get the same speedup for your own interactive ssh/scp/rsync from
the shell, add the equivalent to ~/.ssh/config:
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
Inspect or tear down a master by hand: ssh -O check <host> /
ssh -O exit <host>.
Optional. SSH stays the transport — Tailscale is just the network it rides. This buys the "no public inbound port / identity-based / NAT-traversal" security story with zero application-code change (see research in
docs/wiki/research/orchestration-transport-alternatives.md).
Today every managed machine needs reachable SSH — typically public port 22 or a
hole-punched forward. Tailscale (a managed WireGuard
mesh) gives every machine a stable private 100.x address on your tailnet,
reachable from any other enrolled machine regardless of NAT, with no inbound
port open to the public internet. Point machines.json at the tailnet
addresses and SSH rides the encrypted mesh.
-
Install Tailscale on the orchestrator and every managed machine:
curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up # opens a browser/device-auth link
macOS:
brew install --cask tailscale(or the App Store app), thentailscale up. -
Grab each machine's tailnet address (stable, survives reboots/IP changes):
tailscale ip -4 # e.g. 100.101.102.103 tailscale status # also shows MagicDNS names like gpu-server.tail-scale.ts.net
-
Point
machines.jsonat the tailnet address (or MagicDNS name) instead of the public IP:{ "id": "gpu-server", "host": "100.101.102.103", "user": "ubuntu", "projects_dir": "~/projects" }MagicDNS names work too (
"host": "gpu-server.tail-scale.ts.net") and read better. Verify:ssh ubuntu@100.101.102.103 echo ok. -
Close public port 22. Once every machine reaches the others over the tailnet, drop inbound 22 from the public internet at the firewall / cloud security group /
ufw:sudo ufw allow in on tailscale0 to any port 22 # SSH only over the tailnet sudo ufw deny in to any port 22 # block public 22
(Cloud hosts: remove the
0.0.0.0/0 :22rule from the security group; keep a console/out-of-band path so you can't lock yourself out.)
ControlMaster multiplexing (above) stacks on top — the per-command handshake the mesh adds (a few ms WireGuard overhead) is paid once per host, then reused.
These solve two different exposure problems and compose cleanly:
| Concern | Mechanism |
|---|---|
| Machine-to-machine SSH (orchestrator ↔ managed boxes) | Tailscale mesh — private 100.x, no public port |
| Public access to the portal (your phone, anywhere) | Cloudflare Tunnel — outbound-only, no inbound port (see remote-access.md) |
Neither opens an inbound port on a managed machine. The portal is reachable from the open internet via the tunnel; SSH between machines never leaves the tailnet.
Tailscale SSH can terminate the
SSH connection itself using tailnet device identity / your SSO, dropping
authorized_keys management entirely — access is governed by tailnet ACLs
instead of per-host key files. Enable per node with tailscale up --ssh and an
ACL ssh rule. Not required for the mesh underlay above (plain sshd over
the tailnet works fine); it's a follow-up if you want to retire key
distribution.
Machine configuration is stored in ~/.agentwire/machines.json, keyed under a
machines array (not a bare top-level array):
{
"machines": [
{
"id": "gpu-server",
"host": "192.168.1.50",
"user": "ubuntu",
"projects_dir": "~/projects"
},
{
"id": "do-2",
"host": "167.99.123.45",
"user": "root",
"projects_dir": "~/projects"
}
]
}| Field | Description |
|---|---|
id |
Unique identifier, used in session@machine format |
host |
IP address or hostname |
user |
SSH username |
projects_dir |
Base directory for projects on the remote machine |
Each machine should have a ~/.agentwire/machine/CLAUDE.md — a living document describing that machine's role, services, venvs, paths, and any platform-specific gotchas.
When an AI agent needs to do ops work on a remote machine (manage services, install packages, debug the box itself), spawn a Claude Code session in ~/.agentwire/machine/ rather than SSHing and running ad-hoc commands. The agent picks up both ~/.claude/CLAUDE.md (user preferences) and ~/.agentwire/machine/CLAUDE.md (machine context) automatically, giving it full situational awareness without needing to rediscover everything.
# Spawn an ops session on a remote machine (replace `my-server` with your hostname)
ssh my-server
cd ~/.agentwire/machine
claude # gets both global prefs and machine context
# Or spawn via agentwire from the Mac (remote target is the @machine suffix)
agentwire new -s my-server-ops@my-server -p ~/.agentwire/machine- Machine identity — OS, hardware specs (CPU, GPU, RAM), role in the fleet
- Services — what runs here, how to start/stop/check them, service file locations
- Python venvs — what each venv is for, how to create new ones for the platform
- Key paths — config files, scripts, data directories
- Platform gotchas — WSL paths, sudo requirements, non-standard tool locations
- Install notes — anything non-obvious that tripped you up (saves re-discovery)
~/.agentwire/
├── config.yaml # Main agentwire config
├── machines.json # Registered remote machines
├── voices/ # TTS voice reference files
├── scripts/ # Machine-specific helper scripts
│ ├── tts # TTS management wrapper
│ ├── tts-start # Quick start
│ └── wsl-startup # Boot hook (WSL example)
└── machine/
└── CLAUDE.md # Machine context for AI agents ← THIS
Machine-specific helper scripts live here — TTS management, startup hooks, service wrappers, etc. This is the canonical location. Scripts in ~/bin/ should symlink here so they're on PATH but the source of truth stays in one place.
These scripts are not managed by agentwire and not version-controlled — they're local to each machine because different machines have different roles.
Running agentwire on Windows Subsystem for Linux has a few differences from bare Linux:
- GPU access — CUDA works normally;
nvidia-smiis at/usr/lib/wsl/lib/nvidia-smi(not in default PATH) - Driver location — GPU driver lives on the Windows host; never install Linux GPU drivers
- CUDA toolkit — Install
cuda-nvcc-12-4etc. individually; thecuda-toolkit-12-4metapackage fails on Ubuntu 24.04 (requireslibtinfo5, which is not available) - Systemd — WSL2 supports systemd user services (
systemctl --user); use for persistent services like TTS - Port exposure — Ports are accessible from the Windows host and via SSH tunnels as normal
- Boot hook — WSL doesn't have a traditional init; use a startup script called from Windows Task Scheduler or Windows Terminal profile
# ~/.agentwire/scripts/wsl-startup
#!/bin/bash
sudo service ssh start
systemctl --user start agentwire-tts.serviceLAN exposure. The TTS/STT/Kokoro shims bind
127.0.0.1by default and have no authentication. The unit below passes--host 0.0.0.0so the portal on another machine can reach it — that opens the port to every host on the network. Do it only on a trusted LAN or behind a firewall. The same opt-in via config:tts.host(TTS + Kokoro) andstt.host(STT).agentwire doctorflags a non-loopback shim bind and a remote TTS that is still loopback-bound.
# ~/.config/systemd/user/agentwire-tts.service
[Unit]
Description=AgentWire TTS Server
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/user/projects/agentwire-dev
ExecStartPre=/bin/bash -c 'fuser -k 8100/tcp 2>/dev/null || true'
ExecStartPre=/bin/sleep 2
ExecStart=/home/user/projects/agentwire-dev/.venv-chatterbox/bin/python -m uvicorn agentwire.tts_server:app --host 0.0.0.0 --port 8100
Restart=on-failure
RestartSec=30
Environment=DEFAULT_BACKEND=chatterbox
Environment=CURRENT_VENV=chatterbox
[Install]
WantedBy=default.target