Containers
A container gives an agent a toolchain that is not yours, without giving up your workspace. Reach for it when the agent needs a toolchain or a filesystem you do not have. You name an image, and humanize brings it up on the agent's first turn and removes it with the agent, holding this project directory at the path it already has and running as you, so the work it leaves behind is yours and everything else is the image's.
Try it
- Declare the container beside the tester's place in a flow, so the flow can run the suite in an image that has the right Python:
# .humanize/flows/tested/__init__.py
"""Build here; run the suite in a container that has the right Python."""
from typing import Annotated, NamedTuple
from hmz.agents import AgentBase, Isolated
from hmz.flows import flow
class Agents(NamedTuple):
"""The two this drives, and the two places they work."""
builder: AgentBase # here, and nowhere else
tester: Annotated[AgentBase, Isolated("python:3.12")] # a container of the flow's own
@flow
def run(agents: Agents, task: str) -> None:
working = agents.builder.new()
working(task, suppress=True)
for _ in range(5):
said = agents.tester("Run `python -m pytest -q` and report exactly what failed.",
suppress=True)
if "passed" in said and "failed" not in said:
return
working(f"The suite says:\n\n{said}\n\nFix it.", suppress=True)- Run the flow:
hmz exec -f tested -a claude/claude-opus-5:max -a codex/gpt-5.6-sol:high "get the suite green"The container comes up on the tester's first turn, not when the agent is constructed.
- While it runs, in another terminal:
docker ps --filter label=humanize=$(id -u)You see the container, labelled humanize=<your uid>. It is taken down when the agent is collected, and the workspace is left behind.
The tester runs the suite in its own container, the builder fixes what it reports, and everything they produce lands in your workspace.
From a flow
This is the usual way. The flow writes the image beside the place, and nobody is asked anything:
from typing import Annotated, NamedTuple
from hmz.agents import AgentBase, Isolated
class Agents(NamedTuple):
"""The two this drives."""
tester: Annotated[AgentBase, Isolated("python:3.12")] # a container of the flow's own
reviewer: AgentBase # here, and nowhere elseThe image is the flow's, and the workspace is the directory the flow is running in. Nothing can point that agent anywhere else, including you. The agents page of /flow reads it back on that agent's where row as in a container of python:3.12, with the flow settled this beside it — a row to read rather than one to open.
From Python
Use this for an agent you build yourself, or for a place the flow declared Remote:
from hmz.machines import DockerConfig
ClaudeCodeAgentConfig(model=…, effort=…, machine=DockerConfig(image="python:3.12"))| Field | Default | |
|---|---|---|
image | python:3.12 | Needs a python3 for the target half, plus whatever the agent will reach for. |
workspace | this directory | The directory itself, mounted — not a copy — so the work outlives the container. |
An image with no python3 in it is refused as the container starts, rather than a turn later. The image also needs whatever the agent is expected to reach for: an agent told to run pytest in an image with no pytest spends a turn discovering that. A good image is one you already build for CI.
Where the flow says a place may be pointed anywhere (Annotated[AgentBase, Remote]), you can hand it a container instead:
from hmz.agents import ClaudeCodeAgent, ClaudeCodeAgentConfig
from hmz.machines import DockerConfig
from hmz.runner import Runner
config = ClaudeCodeAgentConfig(
model="claude-opus-5",
effort="high",
machine=DockerConfig(image="node:22", workspace="/home/me/code/myproject"),
)
Runner("movable", [ClaudeCodeAgent(config, name="builder")]).run("upgrade the toolchain")Both refusals land before the first turn:
onbox: reviewer runs on this machine -- this flow does not say it works anywhere else, so it cannot be pointed at one
onbox: tester works in a container of this flow's own, so there is nothing to point it atWhat the container is
- runs as your uid and gid, so files it writes are yours;
- has
HOME=/tmp, away from the workspace, so what a command caches is not the project's; - is reached as a
docker://target, and needs no port and no secret; - is labelled
humanize=<your uid>.
When it comes up, and when it goes
- On the agent's first turn, not when the agent is constructed. Configuring an agent pulls no image and starts no container, so a flow that configures more agents than it drives pulls no image for the ones it does not.
- Shared by every session that agent opens, so its sessions find the workspace as the last turn left it.
- One machine per agent. Two agents built from the same config get one container each.
- Taken down when the agent is collected, or at exit for one held to the end.
- The workspace is left behind either way.
Cleaning up after a flow that was killed outright:
docker rm -f $(docker ps -q --filter label=humanize=$(id -u))The label carries your uid, so this cannot reach past you on a machine several people share.
The agent is still here
This is the same arrangement as remote execution, with the far end a container instead of a host. The agent process stays on this machine, keeping its credentials and its link to its model provider. Everything it does happens in the container, so the container needs no network access to a model provider and no login.
Because the work happens in a mirror rather than in this directory, the backend logs the agent's turns under a path this project has never heard of. It makes no difference: the run wrote down the ids of the sessions it opened, and that is what its trace is gathered by.
hmz trace collectThe run itself is still written down here. A cycle belongs to the directory the flow ran in, and it is a directory of its own with a sessions/ in it. Each session is named for whose it was, what took its turns, which account it ran as and what the backend called it:
run=$(ls -dt ~/.humanize/cycles/*/*/ | head -1) # the run that just finished
ls "$run"sessionsbuilder-claude@local-5f6e7d8c-1a2b-3c4d-5e6f-708192a3b4c5
tester-codex@local-0a1b2c3d-1a2b-3c4d-5e6f-708192a3b4c5The id is the end of the name, and a leading part of it is enough, so the line above collects the tester's, which is the session that worked in the container. At the prompt the same thing is /cycles: enter on the run, then where it is.
Isolation here is about environment, not permission
A container gives the agent a different toolchain and a different filesystem, and mounts your workspace into it. It does not stop the agent editing that workspace.
To narrow what the agent may do at all, that is permissions — a different setting, and they compose:
hmz exec -f tested \
-a cli=codex,model=gpt-5.6-sol,effort=high,permission=workspace-write \
"get the suite green on 3.12"With permission=read-only, the tester is in a container and cannot write anything:
hmz exec -f tested \
-a claude/claude-opus-5:max \
-a cli=codex,model=gpt-5.6-sol,effort=high,permission=read-only \
"get the suite green"Read Security.
Requirements
You need docker on your PATH and a daemon to reach, plus what remote execution needs: Linux on x86-64 here, and a python3 in the image.