Hosts
A host is a machine that runs workers. It can be the laptop you’re typing on, or a headless machine that only takes work: a box in the closet, a spare Mac, or a cloud VM. A laptop runs a couple of workers and stops when the lid closes. A headless host runs many at once and keeps going overnight, so the laptop becomes the place you write orders, not the place they run.
Every machine that sends or receives work has a dispatch config file, and each
host it knows about is one [hosts.NAME] table in that file. A host exists for
a sender when the sender’s config names it. To let teammates send work to a
host you run, see shared hosts.
The config file
hev reads the dispatch config from the first of these that is set:
--config PATHon anyhev dispatchcommand.- The
HEV_DISPATCH_CONFIGenvironment variable. ~/.config/hev/dispatch.toml.
A ~/ prefix expands to the home directory of the machine reading the file.
The same file format is used on senders and targets. A laptop that only sends
work needs host entries. A host that receives work reads its own copy for
admission, notifications and launch settings.
cap = 8 # capacity on this receiving machine
workspace = "~/workspace"
default_host = "buildbox"
[hosts.buildbox]
ssh = "buildbox"
cap = 8
namespace = "team-traces"
[hosts.mac]
ssh = "mac"
cap = 2
path_prefix = ["~/.local/bin", "/opt/homebrew/bin"]
[hosts.winbox]
ssh = "dev@winbox"
os = "windows"
cap = 3
workspace = "C:/src"
harness_wrapper = ["pwsh.exe", "-NoProfile", "-File", "C:/tools/with-creds.ps1"]
[hosts.here]
local = true
cap = 2
Unknown keys are an error. Every field is listed in the config reference.
Reaching a host
A host is either local or reached over SSH. Set exactly one way:
local = trueruns work on the machine that callshev dispatch, with no SSH hop.ssh = "TARGET"is passed tosshas the destination. It can be auser@hostpair or an alias from~/.ssh/config. It may not start with-.
hev runs ssh in batch mode with a five second connect timeout and strict
host key checking. It never prompts. Add the host key to known_hosts and set
up key authentication before you add the host. Each call runs
hev dispatch rpc on the far side, so hev has to be on the PATH that a
non-interactive SSH session sees.
On Unix hosts, path_prefix puts directories in front of that PATH for every
remote call. Use it when hev, codex or claude live somewhere a login
shell adds but a plain ssh host command does not, such as Homebrew’s
/opt/homebrew/bin. Windows hosts ignore path_prefix.
Add a headless machine
Any machine the sending side can reach over SSH works. Setup is the same for a box on your network and a cloud VM.
The target needs:
hev, the same version as the sender. See install.- The harness,
codexorclaude, installed and logged in as the user SSH connects as. Workers run without approvals or a sandbox, so give them a machine and an account you are willing to hand over. - Git, and tmux on Linux or macOS. Windows targets use a detached process instead of tmux; see Windows.
- A workspace with clones. Each repository an order names must already be
cloned under the workspace (
~/workspaceby default) with anoriginremote. Dispatch fetches and creates worktrees; it never clones. - SSH reachable from the sender, with key authentication and a known host key. Connect once by hand first.
- Credentials for pushing, if orders push branches and open pull requests:
a Git credential and
ghlogged in, orGH_TOKENin the SSH environment.
Run the daemon on the target too if you want its runs in your traces. On Linux,
hev up installs a systemd user service; enable lingering to keep it running
after logout.
On a box on your network
Give the box a name in ~/.ssh/config on your laptop, then check that a
non-interactive session finds everything:
Host buildbox
HostName buildbox.example.test
User dev
ssh -o BatchMode=yes buildbox 'hev --help >/dev/null && git --version && tmux -V && codex --version'
In the cloud
Start a VM, install the tools above, and log the harness in once over an
interactive SSH session. Then add the same kind of ~/.ssh/config entry.
Nothing in hev kit is specific to a cloud provider. The VM needs no open
ports beyond SSH.
Add it on the sending side
In ~/.config/hev/dispatch.toml on your laptop:
default_host = "buildbox"
[hosts.laptop]
local = true
cap = 2
[hosts.buildbox]
ssh = "buildbox"
cap = 8
path_prefix = ["~/.local/bin", "~/go/bin"]
Then check it with hev dispatch hosts (below). Send an order with
host: buildbox, or override any order with --host buildbox.
Configure the receiving side
The target can have its own ~/.config/hev/dispatch.toml. It is optional; with
none, the target admits every repository in its workspace. Settings here win
over anything the sender says.
workspace = "~/workspace"
cap = 8
notify = ["notify-command", "{reply}", "dispatch {name}: {state} {summary}"]
[admission]
repos = ["https://example.com/team/project.git"]
[hosts.buildbox]
local = true
[hosts.buildbox.env]
EXTRA_API_TOKEN = ""
[admission] reposis the list oforiginremotes this machine will run. A missing key admits all;repos = []admits none. See admission.[hosts.buildbox]must use the same name the sender uses. The target reads itsharness_wrapperandenvfrom this entry.[hosts.buildbox.env]adds environment variable names to forward to workers. Only the names are used; the values come from the target’s own environment when the order is admitted.notifyruns on the target when a run ends. It is an argument list, run without a shell, with{name},{state},{summary}and{reply}filled in.
Capacity and the queue
cap is the number of runs a host holds at once. It must be at least 1. A
run takes a slot from admission until its supervisor exits. Live slots are
counted on the host itself: tmux sessions named d-NAME on Unix, and checked
supervisor process IDs on Windows. Admission reserves a slot under a short
lock on the target, so two senders cannot overfill it.
When every host an order could go to is full, the order waits in a queue on
the receiving host and dispatch prints its position. The queue is first in,
first out. When a run finishes, its supervisor starts the oldest waiting
order; the daemon’s regular scan (or hev dispatch sweep) picks the queue up
again after a crash. hev dispatch ls shows waiting orders with their
position and age, and hev dispatch stop NAME takes one out of the queue.
Set top-level cap = 8 before any TOML table in the receiving host’s default
config. The target owns this limit. If omitted, it uses its own legacy host
entry’s cap, then defaults to 2. A sender’s optional hosts.NAME.cap can
lower the request’s ceiling but cannot raise target capacity. Size cap to
what the machine can carry: each run is a full agent session plus whatever
builds and tests it starts.
Upgrade all binaries reading the config before adding top-level cap.
Older binaries reject that key. Repeating orders hold their slot between
rounds.
Per-host settings
os = "windows"marks a Windows host. Any other value, or none, means Unix. See Windows hosts.workspacesets where relativerepopaths resolve on this host. Without it, the host uses its own top-levelworkspace, then~/workspace.namespaceis passed to the worker asHEVLAYER_NAMESPACE, so its traces land in that archive namespace. Dispatch does not start or reconfigure capture.harness_wrapperis an argv list run in front of the harness. The wrapper gets--, then the harness and its arguments. It must wait for the harness and exit with its exit code. A Unix wrapper script can end withshift; exec "$@". The host’s own entry wins over the sender’s.[hosts.NAME.env]lists extra environment variable names to forward to the worker on a Unix host.config_pathpoints the host’s supervisor at a different dispatch config, for example an isolated file for a smoke test. A relative path resolves next to the config that names it.
Host names must match ^[a-z0-9][a-z0-9-]*$. Use the same name for a
machine in every config. Browser validation looks up the implementer’s host
by name in the validator’s config.
Checking hosts
hev dispatch hosts
HOST CAP RUNNING FREE REACHABLE VERSION
buildbox 8 3 5 yes dev
here 2 0 2 yes dev
mac: unreachable: ssh: exit status 255: ssh: connect to host mac port 22: Operation timed out; for mutations the outcome is unknown: inspect the named run before retrying
hev asks every host in parallel and gives each one 12 seconds. An unreachable
host takes one line with the SSH error. A host running a different hev
version than yours shows (version mismatch) next to its version. Add
--json for the full response per host, or --host NAME to check one.
hev dispatch run uses the same inventory to pick a host. It prefers
default_host when that host is reachable and has a free slot. Otherwise it
picks the reachable host with the most free slots, breaking ties
alphabetically. When every host is full, the order is queued.