Remote execution
hmz internal anchor runs a coding agent on this machine whose work lands on another one. The agent needs no plugin, no configuration and no cooperation: it is told none of this and takes part in none of it.
The model
There are two arrangements, and which one a session uses is a setting rather than a fact about the target. Everything below that is not marked otherwise is about the first.
An agent runs on this machine, unchanged. Everything it does — reading and writing project files, running commands, reaching the network from those commands — happens on the target.
The workspace the agent works in is a local mirror of the target's copy. It reads and writes the mirror at local speed; humanize keeps the two in step. The mirror lives at the workspace's own path by default, so the paths the agent sees are the target's own.
this machine the target
┌────────────────────┐ ┌────────────────────┐
│ claude / codex … │ │ │
│ ↓ syscalls │ │ hmz internal │
│ ┌──────────────┐ │ one channel │ anchor serve │
│ │ supervisor │──┼──────────────────▶│ ↓ │
│ └──────────────┘ │ ssh / docker / │ files, processes, │
│ local mirror │ tcp / a pipe │ the network │
└────────────────────┘ └────────────────────┘
credentials, the work
the model providerThe other arrangement: --native
The CLI already installed on the target is the one that runs, and it runs there. Nothing is mirrored, nothing is traced, and nothing of humanize is below the agent: this side starts it on the target in the target's own copy of the workspace, carries its three streams byte for byte, sends it the signals aimed here, and exits with its own status.
this machine the target
┌────────────────────┐ ┌────────────────────┐
│ hmz internal │ one channel │ claude / codex … │
│ anchor --native │──────────────────▶│ ↕ │
│ ↕ three streams │ ssh / docker / │ files, processes, │
│ │ tcp / a pipe │ the network │
└────────────────────┘ └────────────────────┘
the flow driving the work, and
the turn the account tooWhich makes running a turn elsewhere an argument change for every backend that already speaks a framed protocol to a process humanize spawns — the frames cross a machine boundary and neither end is told.
hmz internal anchor --native --target docker://build-container --remote-path /srv/project claudeThree things do not follow the CLI across on their own, so the anchor carries each:
- The account. What a provider sets is sent as the turn's environment; what it hushes is taken off on the target with
--hush, where the environment is composed. A variable merely left out of what is sent survives in the target's own shell profile. - Its credential files.
--project NAME=DIRwrites them into a directory on the target that only the target's user may enter, names them to the CLI by the variable that moves the directory they belong in, and removes them when the turn is over. The account leaves this machine, which is the trade this arrangement is: a machine that should not be trusted with it is a machine to reach the other way. A credential the CLI reads out of~/…is the one that does not cross — nothing butHOMEpoints a CLI at one, and a replaced home takes the target's git identity, its ssh keys and the CLI's own transcripts with it. An account kept only there is refused rather than quietly run as the target's own. - The skills the flow carries.
--carry DIR=PATHputs them in the target's copy of the workspace for the length of the turn. Nothing already there is written over, and only what was made is removed.
And one thing cannot: a flow's own callbacks. The bridge carrying them is a program on this machine speaking to a socket in this process, so a turn offering them to a CLI on another machine is refused rather than taken without them. A local target is the exception, the CLI there being here.
Two more things follow from there being no supervisor. The agent's own connections are the target's — there is no --net here, so a provider pointed at 127.0.0.1 is a turn dialling the target's loopback; a gateway a native turn is to use has to be reachable under a name the target resolves. And everything crosses every turn: each turn is a process of its own, so the credentials and the skills are written again each time.
Quick start
hmz internal anchor --target ssh://build-box claude
hmz internal anchor --target ssh://gpu-01 codex exec "run the test suite"Everything after the agent's name is the agent's own. Before running anything, ask the target what it is:
$ hmz internal anchor --check --target ssh://build-box
target ssh://build-box
hostname build-box
python 3.12.3 (pid 41207)
export /home/me/code/myproject -> /home/me/code/myproject
workspace /home/me/code/myproject (184 entries)Every flag is in the CLI reference.
Targets
--target | |
|---|---|
ssh://HOST or ssh://HOST:PORT | Bootstraps the target half over ssh and speaks to it on that connection's pipes. Uses your ssh config, agent and keys. |
docker://CONTAINER | Runs the target half inside a running container over docker exec, as whoever that container runs as. No port and no secret. |
tcp://HOST:PORT | Connects to a target left listening. Cheap to reconnect, which matters for a loop of short turns. |
local or local:DIR | Another directory on this machine, standing in for a remote one. Used for testing, and by the container machines. |
The target half is a zipapp humanize ships to the target and caches there by digest. It needs no installation, and the two halves refuse to run against each other if their versions disagree.
What the agent observes
Inside the workspace it sees the target: the same file names, contents, sizes, modes and timestamps, at the same paths. A failure answers with the target's own error, not a local approximation of it.
Where the target spells a path more than one way, every spelling reaches the same file. A Mac reaches /tmp, /var and /etc through /private, and ignores case unless it was formatted not to, so a path a command there hands back — pwd in a temporary directory answers /private/var/folders/... — is understood as the workspace path it names. A path outside the workspace is left exactly as the agent named it: it belongs to this machine, and one that is not here is reported missing rather than claimed for the target.
Every program it spawns behaves like an ordinary local child — the same descriptors, the same output, the same exit status — and its parent is released as soon as it starts, so commands run concurrently and a long-lived one can be talked to while it runs.
Signals travel both ways: one aimed at a running command reaches the real process on the target, and a command killed there kills its local counterpart the same way.
A command never reports a success it did not achieve: one that cannot be started, or that humanize loses track of, fails visibly. What a command changes on the target becomes visible to the agent once it exits, and when the session ends nothing it started is left running.
What reaches the target
- File contents. A file the agent modifies is pushed in full before any command runs on the target, and again when the session ends.
- Structural changes. Creating, removing, renaming, linking and changing permissions are replayed on the target first, so the target's error is what the agent sees.
- Commands. Everything the agent spawns, including bundled work helpers such as ripgrep, in the target's copy of the working directory.
- Network. Whatever those commands reach.
What stays on this machine
- The agent's own runtime executables and re-execs. For any CLI installed by npm that includes the interpreter its
#!/usr/bin/envline names, at every path onPATHthe search for it may reach; for Codex, the native CLI and its code-mode host besides. - Its state directory, and anything the agent runs from inside it — Grok Build keeps its native binary under
~/.grok/binand re-execs it. All twelve known CLIs are known by name —agy,claude,codex,cursor,dsh,grok,kimi,mimo,opencode,pi,qwen,zcode— as is humanize's own~/.humanize; any other agent keeping state inside the workspace has to be named with--local-path. - Anything named as a local path (
--local-path) or a local program (--local-exec). - The agent's own network connections, so that it can still reach its model provider.
--net remotesends them to the target instead, and--net-allow HOST[:PORT]keeps named hosts local anyway.
Commands the agent spawns always use the target's network, whatever --net says.
Anchoring a flow
Give an agent's config an anchored machine and its turns land there, without any other change to the flow:
from hmz.coganchor.agents import ClaudeCodeAgentConfig
from hmz.coganchor import AnchorConfig
from hmz.coganchor.machines import AnchoredConfig
config = ClaudeCodeAgentConfig(
model="claude-opus-4-8",
effort="high",
machine=AnchoredConfig(
anchor=AnchorConfig(target="ssh://build-box", workspace="/srv/project")
),
)Every option of hmz internal anchor is a field of AnchorConfig and every field is an option, so the two spellings mean exactly the same thing — a flow spawns what an operator would have typed. Settings no session could run under are refused where they are written rather than where they are used, so a flow that misspells a target hears about it as it configures its agents, not hours into the loop.
How often the target is reached depends on the backend. A turn that runs as its own process is anchored on its own, so a loop of short turns reaches the target once per turn — a tcp:// target makes that a socket rather than an ssh session to bootstrap. A backend that holds one process across turns is anchored once for the agent instead.
There is a trade-off worth knowing: an anchored Claude ends its process with each turn, so the turn's work reaches the target before the turn says it landed — at the cost of not being able to hear you during a turn. An anchored Codex keeps one app server for the life of the agent and can be steered throughout, at the cost of that guarantee: its work reaches the target whenever a command runs there, which for a coding agent is constantly, rather than at the end of every turn.
Serving a target
Instead of bootstrapping over ssh each time, a target can be left listening:
# on the target
hmz internal anchor serve --listen 0.0.0.0:7777 --export /srv/project --token "$SECRET"
# on this machine
HUMANIZE_TOKEN=$SECRET hmz internal anchor --target tcp://build-box:7777 --workspace /srv/project claude--export VIRTUAL[:REAL] says which directory to expose, and under what path the agent believes it is using. Repeat it for more than one.
Listening on anything but loopback without --token is refused. Read Security before opening one.
The same program serves both ends — the bundle shipped to a target runs hmz internal anchor serve --stdio, which is one session over a pipe.
From Python
from hmz.coganchor import AnchorConfig, check, connect
config = AnchorConfig(target="ssh://build-box", workspace="/srv/project")
found = check(config) # what the target says about itself; runs nothing there
status = connect(["claude", "--print"], config) # the agent's own exit statusconnect returns once the agent has exited and everything it wrote has been pushed.
AnchorConfig fields map one-to-one onto the flags in the CLI reference: target, workspace, chdir, remote_path, shadow, local_paths, local_execs, redirects, private, net, net_allow, token, force.
Requirements
Running an agent needs Linux on x86-64 or aarch64 and a recent Python. Any other architecture is refused at start-up, and told where it can run instead.
Serving needs only a POSIX system with a Python of the same vintage — no root, no compiler, no kernel module, nothing installed.
What is not guaranteed
Each of these is deliberate, and each looks like a defect if you meet it cold.
- Serving is not a sandbox. An export bounds which files a request may name. It does not confine the commands that request can run, and it does not stop a symlink pointing out of the tree from being followed. A listening port is equivalent to a shell on that machine.
- Mirrored directories are the mirror's, not the target's. A directory in the mirror carries this machine's permissions and the time the mirror was made.
- Only file contents are pushed. A permission change made through an already-open descriptor never reaches the target, and ownership, device nodes and extended attributes never leave the mirror.
- A request that goes unanswered is abandoned here, not there. It may still take effect on the target after the agent has been told it failed.
- Losing the connection does not stop the agent. Work needing the target fails, already-mirrored files still read, and the agent exits with its own status.
- Only the common signals are reproduced faithfully. A repeat of a signal already delivered, and the rarer signals, do not reach the command.
- The mirror is authoritative. Anything in it the target does not have is deleted. humanize refuses a mirror directory holding unrelated files, or one last used against a different target, unless
--forcesays otherwise.
Limits
- Whole files. A file crosses in full, in both directions.
- One writer. The target's workspace must not be edited by anyone else at the same time.
- No privilege escalation.
sudodoes not work below the agent on this machine. Commands run on the target, where it is unaffected. - No crossing. Renaming or linking between the workspace and a path kept on this machine fails.
- 64-bit only. A 32-bit process below the agent is not intercepted and runs against the mirror with nothing replayed.
- Names resolve here and are dialled from the target, so split-horizon DNS can disagree.
Security
An hmz internal anchor port is equivalent to a shell on that machine. Give --token a real secret, and prefer ssh:// or docker://, which need no open port at all.
The full statement, including what running any agent under humanize means, is in Security.