Skip to content

CLI reference ​

The command line of the hmz package: its commands, their arguments and grammars, what each writes to stdout and stderr, the environment variables they read, and their exit statuses. Notation is defined in Conventions.

Synopsis ​

text
hmz                                    open the terminal interface
hmz -h | --help                        list the commands
hmz --version                          print the version
hmz exec <exec-options> [--] <task>    run one flow to its end
hmz web [<options>]                    serve every workspace's runs to a browser
hmz update                             update hmz to its newest release
hmz backup <epic> [--to <target>]...   back epics up to git repositories, or download them
hmz internal <command> [<args>...]     processes Humanize spawns for itself
CommandFunction in hmz.cliSummary as hmz --help prints it
(none)opensOpens the terminal interface.
exec_execrun an agent flow in this directory
web_webopen this directory's runs in a browser on this machine
update_updateupdate hmz to its newest release
backup_backupback epics up to git repositories, or download them
internal_internalinternal commands used by Humanize; do not run directly

python -m hmz is the same program. The console script is hmz = "hmz.cli:main".

Every --help is a summary: no flag's help, command summary, description or epilog runs past thirty words. This page is the full account of each.

Routing ​

hmz.cli.main(argv) routes the line before any command's own parser sees it:

argvResultExit
emptyOpens the interface.0, or 1 (see below)
exactly ["--version"]Prints hmz <version> (from the installed distribution's metadata) to stdout.0
first word is exec, web, update, backup or internalHands every later word to that command unchanged, --help included. No other command's module is imported.the command's
-h or --help firstPrints the top-level help.0
anything elseargparse usage error on stderr (below).2
console
$ hmz bogus
usage: hmz [-h] COMMAND ...
hmz: error: argument COMMAND: invalid choice: 'bogus' (choose from exec, web, update, backup, internal)

hmz --version <anything> is not the version form and fails the same way.

hmz ​

text
hmz

Takes no arguments and no options. Opens the terminal interface in the calling process on the current directory, as it was last left. Starts no run.

Where the runs are held ​

The interface is one frontend of a host. Which host is decided once, at start, in this order:

#ConditionThe interface
1HUMANIZE_DAEMON stripped and lower-cased is off, 0 or noholds the runs in this process. Nothing is said.
2stdin or stdout is not a TTYholds the runs in this process. Nothing is said.
3this machine's daemon is found and its protocol is not 2, or a host an older Humanize left holds this directoryis not opened. See Errors.
4a host for this directory is found through the daemonlinks to it as kind="tui".
5no host is foundstarts one, and the daemon where none is, with hmz.daemon.host(), and links to it.

Steps 3–5 are tried up to 3 times, 0.5 s apart, while linking raises OSError (a host found on its way out). After the third failure the interface holds the runs in this process and prints the last OSError (below).

Holding the runs in this process changes one thing a person sees: /exit with a flow running offers Cancel instead of Detach and exit. See TUI › /exit.

Terminal preparation ​

Before Textual is imported, hmz sets TEXTUAL_DISABLE_KITTY_KEY=1 in its own environment (unless already set) when TMUX is unset or empty and either TERM_PROGRAM=iTerm.app or LC_TERMINAL=iTerm2. See TUI › Keyboard protocol.

Errors and exit ​

ConditionstderrExit
The interface closed normally—0
This machine's runs are held by an older Humanize (daemon protocol not 2), or this directory's by a host one lefthmz: the runs on this machine are held by an older Humanize (pid <n>); stop it with that version, or the runs in <dir> …1
The host let go of the interface (gone)hmz: <why> (or hmz: disconnected from the epics), printed as the interface closes1
No host could be linked in 3 trieshmz: runs cannot be detached from the terminal (<OSError>), so they will run in this process insteadthe interface's

hmz exec ​

Runs one flow in the current directory (the workspace) to its return, with no interface. The run's outworlder is always away.

text
usage: hmz exec [-h] -f FLOW [-a ROLE=SPEC[,...]] [-e ROLE=SPEC[,...]]
                [-p KEY=VALUE[,...]] [--profile-flow] [--profile-environment]
                [--resume] [--json]
                [--backup NAME]
                task

Options ​

OptionValueOccurrencesDefaultMeaning
-f, --flow<ref>≥ 1 (required; the last wins)—The flow.
-a, --agents<agent> list0‥n, mergednoneOne agent per agent role.
-e, --envs<env> list0‥n, mergednoneOne environment per environment role.
-p, --params<param> list0‥n, mergedthe flow's defaultsFields of the flow's FlowParams, and, as budget.<limit>, what the run may spend: required except for chat.
--profile-flowflag0‥1offProfile the flow's own Python, as well as tracing the run.
--profile-environmentflag0‥1offProfile the programs the run's agents start, as well as tracing them.
--resumeflag0‥1offPick up the newest run of this flow here.
--jsonflag0‥1offWrite the run as NDJSON on stdout.
--backupa backup target name, or none0‥n, mergedbackup.defaultsWhere the epic is backed up; none anywhere: nowhere. A name settings has none of exits 2. Once the run has said how it went, hmz exec waits up to 60 s for the epic's backups, saying hmz exec: backing the epic up on stderr, and says where it left one.
-h, --helpflagPrint the help and exit 0.
taskpositional stringexactly 1 (required)—What the flow is to do. Put -- before a task that starts with -.

Options may appear in any order and before or after task. No option has an environment variable equivalent.

Grammar ​

Every list-valued option is read by one grammar. Terminals are quoted; ? is optional, * zero or more.

text
option-value  = item , { "," , spaces , item } ;          (* split: see Items *)
key           = ( letter | "_" ) , { letter | digit | "_" | "-" } ;
identifier    = ? a Python identifier (str.isidentifier) ? ;

agent         = identifier , "=" , cli , ( "@" , provider )? , "/" , model , ( ":" , effort )? ;
cli           = ? a CLI name or alias, or an ACP CLI's name; contains no "@", "/" or "," ? ;
provider      = ? an account name; non-empty after "@" ? ;
model         = ? any text, may contain "/" and ":" ; non-empty ? ;
effort        = ? any text without ":" ; "auto" means the CLI's default ? ;

env           = identifier , "=" , backend , ( "@" , ( runtime | "[" , host , "]" ) )? , workdir? ;
backend       = "local" | "ssh" | "docker" | "swarm" | "apple-container" | "slurm" ;
runtime       = ? the name of a runtime saved for that backend ? ;
host          = ? [user@]host[:port], or an ssh config alias; ssh only ? ;
workdir       = "/" , ? any text ? ;                       (* "/~" or "/~/…" is home-relative *)

param         = key , "=" , ? any text ? | limit ;
limit         = "budget." , ( name | "graceful" , ( "." , name )? ) , "=" , ? value ? ;
name          = "duration" | "cost" | "output_tokens" ;

Items ​

  • Each occurrence of -a, -e or -p is split into items at every , that is followed, after optional whitespace, by a key=, budget.key= or budget.graceful.key= (regex ,\s*(?=(?:budget\.(?:graceful\.)?)?[A-Za-z_][\w-]*=)). A comma not followed by one belongs to the value: -p tags=a,b,c is one item.
  • A value therefore cannot contain ,<key>=.
  • Items of every occurrence of one option form one list, in the order written: -a x=…,y=… and -a x=… -a y=… are the same line.
  • An item that is empty or only whitespace is refused: <flag> '<value>': an item is empty.
  • Leading and trailing whitespace around an -a or -e item, and around a -p key, is ignored; a -p value is kept as written (a budget.<limit> value is read stripped).

Naming a flow (-f) ​

text
ref = [ "@" , flowverse , "/" ] , [ user , "/" ] , flow , [ ":" , name ]
    | path
    | "git+" , url , [ "@" , rev ] , [ "#" , subdir ] , [ ":" , name ] ;
path = ( "." | "/" | "~" ) , { character } ;
FormResolves to
<flow>The first flow of that name, looking in local, user, official (built in, then installed), then the flows installed from added flowverses (the order of Flowverses.nearest).
<user>/<flow>The flow installed from the official flowverse's flows/<user>/<flow>/: alice/kernel.
@<flowverse>/<flow>, @<flowverse>/<user>/<flow>The flow installed from that flowverse. @local/… is ./.hmz/flows/, @user/… is ~/.hmz/flows/.
…:<name>Another flow defined in the same module.
a path (./x, ../x, /x, ~/x, ./x.py)That directory or file. Only what starts with ., / or ~ is a path.
git+<url>[@<rev>][#<subdir>][:<name>]The flow in <subdir> of a repository (its root without #), cloned and pinned at that revision, installing nothing: git+https://github.com/humanfia/humanize1-flow@v0.1.0#humanize1:rlcr.

A flow a flowverse's index lists is not run until it is installed (hmz exec installs nothing): naming one is refused with <name>: not installed -- install it from /flow (flowverse <flowverse>). A name said as before the @ is refused saying how it is said now (local/x: a flow of local is called @local/x now), as is a relative path without its ./ (flows/x: a path starts with ./, / or ~, as ./flows/x does). Resolution, module loading and the several-flows-per-module rule are specified in Flows › Where flows live and Flows › Refs.

Writing an agent (-a) ​

text
<role>=<cli>[@<provider>]/<model>[:<effort>]
PartRule
<role>A Python identifier: a field of the flow's agents declaration. Required on the command line.
<cli>Read from the front up to the first /, then split at the first @. A CLI name or alias below, or the name of an ACP CLI.
@<provider>An account of that CLI. @ followed by nothing is refused. Absent: the CLI runs as this machine's own login (as local).
<model>Everything after the first /, less a trailing :<effort>. Passed to the CLI unchecked. May contain / and :.
<effort>After the last :, where what follows it is spelled as an effort: words of letters joined by -, _ or a space (high, extra-high, as configured), or nothing. Otherwise -- custom_provider:gateway/m, qwen3:8b -- the : is the model's, and so is everything after it. Left off, or auto, or empty: no rung, stored as "". A model whose own name ends in :<word> is written with its effort after it: qwen3:latest:auto. Any other word must be on the CLI's ladder (Agents); an ACP CLI takes any word.
<cli>AliasesLadder, hardest firstInstalled byExtra
agyantigravityhigh medium low—
claudeclaude-codeultracode max xhigh high medium lownpm i -g @anthropic-ai/claude-code
codexultra max xhigh high medium lownpm i -g @openai/codex
cursor-agentcursor-climax xhigh extra-high high medium low minimal nonecurl https://cursor.com/install -fsS | bash
dshdeepseek-harnessmax high low offpip install 'deepseek-harness-sdk>=0.1.5rc1,<0.1.6' 'python-dotenv>=1.2.3'hmz[dsh]
grokgrok-build, grokbuildxhigh high medium lownpm i -g @xai-official/grok
kimikimi-codemax high medium lownpm i -g @moonshot-ai/kimi-codehmz[kimi]
mcodeminimax, minimax-codemax xhigh high medium lownpm i -g @minimax-ai/code
mimomimocode, mimo-codexhigh high medium low minimalnpm i -g @mimo-ai/cli
ompoh-my-pimax xhigh high medium low minimal offcurl -fsSL https://omp.sh/install | sh
opencodexhigh high medium low minimalnpm i -g opencode-ai
pimax xhigh high medium low minimal offnpm i -g @earendil-works/pi-coding-agent
qwenqwen-codemax xhigh high medium low nonenpm i -g @qwen-code/qwen-code

Rungs a CLI takes beyond its listed ladder (such as Kimi Code's swarm… rungs) are in Agents. A role's permission, skills and required capabilities are declared by the flow and are not part of -a.

Writing an environment (-e) ​

text
<role>=<backend>[@<provider>][/<workdir>]

Parsed by the regex (?P<role>[^=]*)=(?P<backend>[^@/\[\]]*)(?:@(?P<provider>\[[^\]/]*\]|[^/]*))?(?P<at>/.*)?. An @ is written only before a provider.

PartRule
<role>A Python identifier.
<backend>local, ssh, docker, swarm, apple-container or slurm.
<provider>Absent: this machine — a directory here for local, docker's default daemon here for docker, the swarm this machine manages for swarm, this Mac's Apple containers with nothing saved for apple-container, the Slurm cluster this machine submits to, with its defaults, for slurm. local takes no provider at all; ssh always takes one. @<name>: the runtime saved under that name for that backend; a name nothing is saved under is refused. @[<host>], for ssh only: a host nobody saved, [user@]host[:port] or an alias of the ssh config, handed to ssh as it is; it never stands for a saved runtime and has no fallback list.
<workdir>From the first / after the provider. /~ and /~/… are relative to the ssh login's home for ssh, and to this user's home for local and for docker, swarm, apple-container or slurm on this machine. Omitted: the saved runtime's own workdir, refused where no runtime is named or it was saved with none. For docker, a directory of the daemon's host, mounted into the container at the same path; for swarm, one every node its task may land on has, likewise; for apple-container, a directory of this Mac, mounted likewise; for slurm, a directory of a shared filesystem, at the same path here and on every node its job may land on.

local/home/me/repo, docker/srv/repo, swarm/srv/repo, apple-container/Users/me/repo and slurm/lustre/me/repo are this machine's; ssh@gpu-box/~/repo, docker@gpubox/srv/repo, swarm@cluster/srv/repo, apple-container@mac/Users/me/repo and slurm@hpc/lustre/me/repo are saved runtimes, and ssh@gpu-box alone is the workdir gpu-box was saved with; ssh@[me@far.host:2222]/srv/repo is a host nobody saved. A spelling -e refuses with a hint — local@/x, docker@local/x, swarm@local/x, apple-container@local/x, slurm@local/x, an unsaved ssh host out of brackets — is read as the one the hint gives where it was kept in settings or an epic (docker@local, swarm@local, apple-container@local, slurm@local only where no runtime is saved as local).

Roles the runtime fills — LocalEnv roles, which are the workspace — are never given. After parsing, every environment is opened and probed before the flow is called; an unreachable one, or one whose machine has fewer CPUs, GPUs or less memory than its role declares, is refused. A docker environment's container is started then, a swarm environment's service created and waited for until its task runs, and a slurm environment's job submitted and waited for until it runs -- every environment at once, the first refusal stopping the rest. Where the provider is a saved runtime with a fallback list, such an environment moves down that list first, saying so on stderr:

text
hmz exec: docker:a cannot hold 'box': <why>; using docker:b

Writing params (-p) ​

text
<key>=<value> | budget.<limit>=<value>
  • <key> matches [A-Za-z_][\w-]*; each key at most once across every -p.
  • <value> is everything after the first =, unstripped. It is validated by the flow's FlowParams model: read as the field's type (3, false), otherwise as JSON.
  • Keys not given keep the field's default. Unknown keys and values the model rejects are refused with the model's validation error, prefixed <canonical ref>:.
  • budget is the run's, never a flow's: budget.<limit> sets one limit of the run's budget, budget alone is refused, and a flow whose FlowParams has a field or alias budget is refused when it is defined.

The budget (budget.*) ​

text
budget.duration=<duration> | budget.cost=<usd> | budget.output_tokens=<count> | budget.graceful=<bool>
budget.graceful.duration=<bool> | budget.graceful.cost=<bool> | budget.graceful.output_tokens=<bool>

Each limit at most once across every -p, among the flow's params or apart from them: -p rounds=3,budget.cost=5 -p budget.duration=1h. At least one of duration, cost, output_tokens must be set. The first limit reached stops the run. See Flows › What a run may spend.

LimitAcceptedType
durationSeconds as a number (90, 1.5); units w d h m s concatenated, each at most once, decimals allowed (1h30m, 1.5h, 2d); ISO 8601 (PT1H30M); HH:MM:SS. Finite, ≥ 0.timedelta
costUSD, optional leading $; inf for no limit. ≥ 0, not NaN.float
output_tokensInteger with optional _, or a decimal with k (×1 000) or m (×1 000 000) that comes to a whole number (200k, 1.5m).int
graceful1 true yes on / 0 false no off, case-insensitive. Default true: the turn under way when a limit is reached is let finish.bool
graceful.duration, graceful.cost, graceful.output_tokensAs graceful, for that limit alone, over what graceful says: budget.cost=5,budget.output_tokens=200k,budget.graceful.output_tokens=false cuts a turn off at its 200 000th token but lets one that reaches $5 finish.Graceful

With no budget.*, chat runs under Budget(cost=inf); any other flow is refused, the other flows Humanize ships included.

A run with a finite cost limit first brings the price list up to date when the copy kept is missing or older than 24 h (at most 20 s; never with HUMANIZE_PRICES=off). A cost limit over agents whose model still has no known price cannot be enforced. The run starts, and stderr first carries:

text
hmz exec: nobody lists a price for <model>[, <model>…], so cost=<n> cannot stop what it spends

A price list that gives any model a price that is not a finite number (NaN, an infinity) refuses the run instead, whatever its budget: hmz exec: error: the price list … and exit status 2, before the first turn.

Where the harness runs ​

The harness is an agent's CLI and its supervisor. Nothing on the line says where it runs: no option of hmz exec takes it. Work on this machine (env location local) has its harness here; work on another machine (env location standalone) has it where the harness setting of the saved runtime it is on says, and nowhere else: env (the default, and the setting of a machine nobody saved) on the environment's machine, natively; local here; ssh:<name>, docker:<name>, swarm:<name>, apple-container:<name> or slurm:<name> on another saved runtime.

Once the environments are probed, before the flow is called, every agent's harness is settled on every machine; a setting that cannot hold it (the CLI missing there, a fence it cannot hold, a hook gating the agent's tools under env, a harness runtime that cannot be reached) refuses the run (hmz exec: error: <backend>[@<provider>]: <cli>'s harness cannot run where its harness setting (<setting>) says: <refusal>, exit 2). A runtime a harness setting names is opened as an environment is, probed before the flow is called and closed with the run. Where each session's harness went is recorded on the session in the epic as local, env or <backend>:<name> (Tracing › Epics), and in the daemon's opened record. Detail: Remote execution › Where the harness runs.

Picking a run up (--resume) ​

--resume picks up the newest epic of the same flow (matched by canonical ref) in this workspace whose journal (resume.jsonl) holds at least one entry and whose run is not still going (in another terminal, or held by hmz; see .held). The flow must be resumable. -a, -e and -p are still read from the line; the budget counts from zero. The new run is a new epic and records the epic it picked_up. Without --resume every run starts from the top.

RefusedMessage
the flow is not resumable<flow> does not support resuming, so there is no run to resume
no epic of it here has a journal entry<flow> has no run to resume here: none saved any progress

Processing order ​

StageChecksOn failure
1. argparseOptions, -f and task present.Usage on stderr, hmz exec: error: <why>, exit 2.
2. Spec parsing (Hmz.read)Every -a, -e, -p against its grammar, and every -e provider against the runtimes saved; duplicate roles and keys.Same as 1.
3. Loading (Hmz.run → Runner)Flow resolves and loads; roles, harness kinds, capabilities, effort ladders, params, budget presence, --resume.hmz exec: error: <why> (no usage), exit 2.
4. Opening (Run.run, before the flow is called)Environments reached and measured; each agent's harness settled where its runtime's harness setting puts it, and the runtimes it goes to reached; skills fetched.hmz exec: error: <why>, exit 2.
5. The flow—See Exit statuses.

Nothing of an agent has started before stage 5. What a flow module does when imported (stage 3) is the module's own.

What is refused before anything runs ​

Every message below is printed after hmz exec: error: on stderr, with exit status 2. Stage 1–2 messages are preceded by the usage block.

RefusedMessage
no -f, no taskthe following arguments are required: -f/--flow / …: task
unknown optionunrecognized arguments: <args>
an -a item without role=-a '<item>': expected <role>=<harness>[@<provider>]/<model>[:<effort>]
an -a role that is not an identifier-a '<item>': '<role>' is not a place a flow could declare: what is written before `=` is a field of the tuple of agents the flow declares, so it is a Python identifier
an -a with an unknown CLI, no / or no model-a '<item>': expected [NAME=]CLI[@PROVIDER]/MODEL[:EFFORT]
@ with no account-a '<item>': expected an account after @, as in claude@deepseek/MODEL:EFFORT
a role twice in -a-a: the role '<role>' is given twice
an -e that does not match-e '<item>': expected <role>=<backend>[@<provider>][/<workdir>]
no /<workdir>, and no runtime saved with one-e '<item>': expected <role>=<backend>[@<provider>][/<workdir>]; /<workdir> may be left off only for a runtime saved with one
an -e role not an identifier-e '<item>': the role '<role>' is not an identifier
unknown backend-e '<item>': '<backend>' is not a backend; one of local, ssh, docker, swarm, apple-container, slurm
ssh with no host-e '<item>': ssh needs a host: ssh@<saved host>/<workdir>, or ssh@[user@host:port]/<workdir> for a host not saved
ssh naming a host nobody saved, out of brackets-e '<item>': no ssh host is saved as '<name>'; write <role>=ssh@[<name>]/<workdir> for a host not saved
a bracketed host that is not one-e '<item>': '<host>' is not an ssh host, as [user@]host[:port]
brackets on docker, swarm, apple-container or slurm-e '<item>': only ssh takes a host nobody saved; <backend>@<name> names a <backend> runtime saved on the runtimes page of /settings
docker@local, swarm@local, apple-container@local, slurm@local, nothing saved as local-e '<item>': <backend> on this machine names no provider; write <role>=<backend>/<workdir>
docker, swarm, apple-container or slurm naming a runtime nobody saved-e '<item>': no <backend> runtime is saved as '<name>'; save one on the runtimes page of /settings, or write <role>=<backend>/<workdir> for <backend> on this machine
docker@, swarm@, apple-container@ or slurm@ with nothing after it-e '<item>': an @ is written only before a provider; write <role>=<backend>/<workdir>
local with an @-e '<item>': local takes no provider; write <role>=local/<workdir>
a role twice in -e-e: the role '<role>' is given twice
a -p without key=-p '<item>': expected <key>=<value>
a -p key twice-p: '<key>' is given twice
budget with no limit-p '<item>': a budget is given a limit at a time, as budget.cost=5
an unknown budget. limit-p budget.<limit>: not a limit; one of budget.duration, budget.cost, budget.output_tokens, budget.graceful, budget.graceful.duration, budget.graceful.cost, budget.graceful.output_tokens
a budget. value-p budget.duration: '<v>' names a unit twice, -p budget.duration: '<v>' is negative, -p budget.duration: duration '<v>' is too long, -p budget.duration: '<v>' is not a valid duration: must be finite and not negative, -p budget.duration: '<v>' is not a duration: use seconds, 1h30m, or ISO 8601 like PT1H30M, -p budget.cost: '<v>' is not a valid USD cost, -p budget.output_tokens: '<v>' must be a whole number of tokens, -p budget.output_tokens: '<v>' is not a valid token count: expected a number like 200000 or 200k, -p budget.graceful: '<v>' must be true or false
a budget that limits nothing-p budget.*: Value error, a budget sets at least one of duration, cost, output_tokens
no such flow<ref>: no flow is called '<ref>' (a path: <ref>: there is no flow at <ref>); <ref>: a flow of local is called @local/<flow> now; <ref>: a path starts with ./, / or ~, as ./<ref> does; <ref>: not installed -- install it from /flow (flowverse <flowverse>); <ref>: the official flowverse has not been fetched yet -- fetch it from /flow
a role the flow does not declare<flow> has no agent role '<role>'; available roles are '<a>', '<b>' (… environment role … for -e; none where there are none)
a role the runtime fills<flow>: '<role>' is assigned automatically by the runtime and cannot be set with -a; <flow>: '<role>' is the workspace the run started in and cannot be set with -e
a required role unfilled<flow> needs an agent for '<role>'; specify each with -a ROLE=CLI/MODEL:EFFORT; <flow> needs an environment for '<role>'; specify each with -e ROLE=BACKEND[@PROVIDER]/WORKDIR
a role typed as one CLI given another<flow>: '<role>' requires <cli>, but got <cli>
an @<provider> naming no account of that CLI<flow>: '<role>' names no <cli> account called '<provider>'; make it on the accounts page of /settings
a CLI lacking a capability the role needs<flow>: '<role>' needs <Mixin>[, <Mixin>…], which <cli> does not support
an effort off the ladder<role>=<spec>: <cli> cannot be asked to think at '<effort>'; expected one of <ladder>
params the flow rejects<canonical ref>: <pydantic validation error>
no budget.*, any flow but chat<flow> requires a budget: specify with -p budget.cost=...,budget.duration=...,budget.output_tokens=...
--resumesee Picking a run up
an environment unreachable or smaller than declaredthe reason, naming the role
a harness that cannot run where its runtime's harness setting sayssee Where the harness runs
a skill a role names that cannot be found or fetchedthe reason, naming the skill

Output ​

StreamTerminal (--json off)Piped or redirected (--json off)--json
stdouteach result's text, only where stdout and stderr are not both terminalseach result's text, one write per turn, followed by \nNDJSON objects only; sys.stdout is redirected to stderr for the run
stderrthe run, styled, with a live clockthe run, same lines, no escapes, no clocklines printed by the flow; hmz exec: … notes

Escapes are written to a stream only where colours says so; the clock is drawn only where stderr is a terminal and escapes are allowed.

Human-readable lines ​

One line (or block) per event, in order. ● is ⏺ on macOS.

Event kindDrawn asStyle
begins● <agent> is working[ · conversation <i> of <n>] (suffix only where the agent holds ≥ 2 sessions)dim
text● <first line>, further lines indented two spacesbullet green
reasoningeach line as isdim italic
tool● <name>(<rest of text>)bullet green, arguments dim
subagent, subagent-ends● <name>(<about>) started / … donebullet cyan
notice● <text>bullet yellow, text dim
asks● <text>yellow
failedhmz: <text>red
resultcost footer (below), then the answer on stdout as abovedim
ends✻ Worked for <s>s · <agent>dim
tooknothing

Cost footer: ✻ <kind> <count>[ · <kind> <count>…] · [<money> · ]<model> · <agent>, drawn only for a result carrying spent. <model> is the one model the result's tokens names, where it names exactly one (the model a fallback carried the turn to, or the one an alias resolved to), else the agent's configured model; it is also what the money is priced at. Kinds in the order input, output, cache_read, cache_write, reasoning, then any other alphabetically. Counts: < 1000 as an integer, < 1 000 000 as <n.n>k, else <n.nn>M. Money is omitted for a model with no known price; otherwise $<n> with no decimals from $100, two decimals from $0.01, four below, $0.00 for zero.

Clock (terminal only, 10 frames/s, spinner dots, removed when the run ends): <agent> is working · <s>s for one open turn; <n> turns working · <s>s for several, timed from the oldest.

--json ​

One JSON object per event, UTF-8, one per line, flushed as written. Every object carries every key:

KeyTypeValue
atnumberUnix time the record was made.
agentstringThe agent's own name, such as NeiKos496: never the role it fills.
clistringThe backend (claude, codex, …, or an ACP CLI's name).
modelstringThe model it was configured with.
sessionstringThe backend's own id for the conversation; "" before it has named one, and for events not about one conversation.
kindstringOne of the kinds below.
textstringThe words.
whosestringFor subagent and subagent-ends, the backend's id for that sub-agent; else "".
tokensobjectOn result: {model: tokens} from a backend that reports it; else {}.
spentobjectOn result: tokens by kind (input, output, cache_read, cache_write, reasoning), only the kinds the backend reports; else {}.
kindMeaning
begins, endsA turn starts; a turn is over.
textThe agent's words.
reasoningThe agent thinking aloud.
toolA tool call.
subagent, subagent-endsA sub-agent the agent started, and its end.
asksThe agent asking its user a question.
tookA line put into the running turn is now in front of the model; text is that line.
noticeHumanize about the turn: a rate limit waited out, another account taking over, a turn cut off or ended for silence.
failedThe turn failed; text is why. Closes the turn.
resultThe answer the turn ends on. Closes the turn.

Lines that are not events (hmz exec: … notes, the flow's own prints) go to stderr.

console
$ hmz exec -f chat -a assistant=claude/claude-opus-5:high --json "say hello" | jq -c 'select(.kind == "result")'
{"at":1789026740.6,"agent":"NeiKos496","cli":"claude","model":"claude-opus-5","session":"1d1ff959","kind":"result","text":"Hello.","whose":"","tokens":{"claude-opus-5":2080},"spent":{"input":2000,"output":80}}

Colour ​

Whether escapes are written to a stream is settled once, in this order; the first that applies wins:

#ConditionEscapes
1NO_COLOR set and non-emptyno
2TERM=dumbno
3FORCE_COLOR set, non-empty and not 0yes
4the stream is a TTYyes
5otherwiseno

FORCE_COLOR does not make a stream count as a terminal: the layout stays the piped one and no clock is drawn. rich is imported only where escapes are written.

Nobody is at the prompt ​

The run's outworlder is away for the whole run. A flow's question is answered at once: "" for text, the schema's defaults for a model, or OutworlderAway where a field has no default. An agent's own question is answered with nobody. asks events still appear in the output.

Signals ​

SignalEffectExit
SIGINT, SIGTERM, SIGHUPThe run is stopped as by Run.stop() (from a helper thread). The process waits until the run has let go of what it made — sessions closed, containers removed. hmz exec prints no traceback of its own. A signal already set to SIG_IGN (a hangup under nohup, an interrupt to a background job of a non-interactive shell) stays ignored.128 + signum of the first: 130, 143, 129
a second SIGTERM or SIGHUPIgnored while the run lets go.as the first
a second SIGINTEnds the process at once, by SIGINT itself, without waiting for the run to let go: what it made may be left behind.130 (killed by SIGINT)

An interrupt that arrives while the flow is still being loaded, or after the run has ended, also exits 130 without a traceback.

A run that ends because its budget was reached is not a failure: stderr gets hmz exec: stopped -- <why> naming the limit, and the exit status is 0.

hmz web ​

text
hmz web [--port <port>] [--no-open] [--attached]
OptionDefaultMeaning
--port <port>0The port to listen on, on 127.0.0.1 and, where this machine has IPv6, on ::1; 0 picks any free one on both.
--no-openoffPrint the address and open no browser.
--attachedoffStop once standard input closes.

Serves this directory's runs to a browser on this machine until SIGINT, SIGTERM or SIGHUP -- or, with --attached, until standard input closes, which is how a program that started it takes it down even when that program crashes or is killed -- as one more frontend of them, named browser, of kind web. Once listening it prints hmz web: http://127.0.0.1:<port>/?key=<key> to stdout and opens that address in a browser. The runs are held as hmz holds them, but for the terminal: in this process where HUMANIZE_DAEMON is off, 0 or no, by this directory's host process otherwise. What it serves, refuses and answers is the Web reference.

ConditionstderrExit
Interrupted, terminated or hung up on, or, attached, standard input closed—0
An older Humanize holds the runshmz: the runs in <dir> are held by an older Humanize (pid <n>); stop it with that version1
The runs could not be reached, or the port listened onhmz: the web interface cannot be served: <OSError>1

hmz update ​

text
hmz update

Asks PyPI for the newest release of hmz (info.version of https://pypi.org/pypi/hmz/json, which skips pre-releases) and, where it is newer than the one running, has whatever installed this hmz install it:

Installed byRecognised byRuns
uv tool installuv-receipt.toml in sys.prefixuv tool upgrade hmz (keeps the extras)
pipx installpipx_metadata.json in sys.prefixpipx upgrade hmz (keeps the extras)
anything else—python -m pip install --upgrade hmz[<extras>], or uv pip install --python <python> --upgrade hmz[<extras>] where there is no pip; <extras> are those whose packages are all installed

It then asks a fresh interpreter which version is installed, and prints Updated hmz to <version>. A uv tool installed with a pin (uv tool install hmz==<v>) is held to it, as uv tool upgrade holds it; uv says how to lift the pin. Epics a daemon already holds keep running under the release it started with until it stops; where one is running, hmz update says so and leaves it alone. The interfaces say when a newer release is out: the terminal interface in a line as it opens, the web interface with Update in the daemon picker.

ConditionOutputExit
Already the newest releasestdout hmz <version> is the newest release; nothing is run0
Updatedstdout Updating hmz <old> to <new>: <line>, the installer's own output, Updated hmz to <new>0
Installed from a checkout or a URL (direct_url.json)stderr hmz update: hmz was installed from <url>, not PyPI; update it there, the URL without its user, password, query or fragment1
The installer is not on PATHstderr hmz update: <uv|pipx> installed hmz but is not on PATH1
PyPI did not answer, or HUMANIZE_RELEASES=offstderr hmz update: PyPI could not be asked for the newest release1
The installer failedstderr hmz update: <installer> exited <status>1
The installer left another versionstderr hmz update: <installer> left hmz at <version>, not <new>1

hmz backup ​

text
usage: hmz backup [-h] [--to NAME] [--list | --pull EPIC] [epic]

Backs one epic of this directory -- by its name, or by its path -- up to git repositories now, waits for it, and says how each target took it. With --list or --pull, reads the targets instead: each is fetched first.

OptionValueOccurrencesDefaultMeaning
epicthe epic's name or pathexactly 1, without --list and --pull; 0 with them—The epic.
--toa backup target name0‥nbacking up: the targets the epic was started with, else backup.defaults; --list, --pull: every targetWhere it goes, or where to look.
--list—0‥1offList each target's epics, of every machine.
--pullan epic's name, or <machine>/<workspace>/<epic>0‥1—Download it from the first target holding it, as an import.
ConditionOutputExit
Every target took itstdout <target>: backed up to <url>, the URL with anything signed into it taken out0
A target did notstderr <target>: failed: <why>1
No such epic, no such target, or no target at allargparse usage error on stderr2
--liststdout <target>: <n> epics, then per epic <machine> <workspace> <epic> <how or unfinished> <task>0
--pull, downloadedstdout the path it now has0
A target could not be fetchedstderr <target>: failed: <why>, the rest still read1
--pull of an epic already herestderr hmz backup: <epic> is already here, at <path>1
--pull of an epic no target holds, or more than one machine's of that nameargparse usage error on stderr2

hmz internal not typed by hand ​

text
hmz internal <command> [<args>...]

The command lines Humanize starts its own processes with. Each still runs when typed. hmz internal -h prints the help below and exits 0. hmz internal with no command, or an unknown one, prints the usage and an argparse error (the following arguments are required: COMMAND, invalid choice: …) and exits 2.

CommandSummary as printedSpawned by
anchorrun an agent turn on another machineevery turn whose work lands on another machine
clirelay agent runs of a flow's command line to the flowa script on an agent's PATH, once per run of a flow's command line
credrun a program with credentials from an accountevery turn or login under an account, and every turn whose sessions Humanize keeps
fencerun a program held to what its flow permitsevery turn of an agent whose permission fences anything
hookrelay agent hooks to a flowa CLI's hook table, once per hook call
toolsrelay agent tool calls to a flowa CLI's MCP configuration

hmz internal anchor ​

text
hmz internal anchor [options] AGENT [ARGS...]
hmz internal anchor serve …
hmz internal anchor rendezvous …

Runs a coding agent here whose work lands on another machine. A first word of serve or rendezvous selects the subcommand; otherwise every word after the options is the agent and its own arguments. Loads coganchor and nothing else of Humanize. Semantics: Remote execution.

OptionDefaultMeaning
--target URL$HUMANIZE_TARGET, else localssh://HOST, docker://CONTAINER[?OPTIONS][@ENDPOINT], apple-container://CONTAINER, slurm://JOBID[?python=PATH], tcp://HOST:PORT, peer://TICKET@HOST:PORT, or local[:DIR].
--harness WHERE$HUMANIZE_HARNESS, else localWhere the agent process and its supervisor run: local, same (wherever --target is), or a target spelling.
--broker HOST$HUMANIZE_RENDEZVOUS, else this machine's outward-facing addressWhere the two halves dial to be introduced, when harness and target differ.
--workspace PATHthe current directoryThe project directory as it exists on the target.
--chdir PATH--workspaceWhere inside the workspace the agent starts.
--remote-path PATH--workspaceWhere the workspace really lives on the target.
--shadow PATH$HUMANIZE_SHADOW, else --workspaceThe mirror directory on the harness's machine.
--local-path PATH—Keep this path on this machine even inside the workspace. Repeatable.
--local-exec PATH—Run programs under this path here. Repeatable.
--redirect FROM=TO—Answer this path (file, or directory subtree) with that one, kept local. Repeatable.
--private NAME—Keep this variable out of the agent's commands on the target. Repeatable.
--net {local,remote}localWhere the agent's own TCP connections go. Commands always use the target's network.
--net-allow HOST[:PORT]—With --net remote, keep connections to this host local. Repeatable.
--token TOKEN$HUMANIZE_TOKENShared secret a tcp:// target expects.
--token-file FILE—Read that secret from FILE instead. What Humanize itself spawns is always given this, never --token: a command line is in every user's ps. The file is under hmz.machine()/tokens/, mode 0600, named by the secret's SHA-256.
--forceoffUse the mirror directory even if it holds unrelated files.
--nativeoffRun the CLI installed on the target instead of supervising one here.
--hush NAME—With --native, unset this variable for the CLI on the target. Repeatable.
--project NAME=DIR—With --native, put this credentials directory on the target for the session and set NAME to it. Repeatable.
--bring NAME/PATH=DIR—With --native, copy what the turn wrote at PATH (may be a glob) inside the directory --project set NAME to back into DIR, at the same place, before that directory is removed. Repeatable.
--tell DIR—With --native, keep a file of NAME=PATH lines in DIR on this machine while the turn runs, one per --project that landed at PATH on the target.
--installs LINE—With --native, the install line to report where the target lacks the CLI.
--fence JSON—Hold the agent to this fence on both machines.
--checkoffConnect, print what was found, exit.
--log-level {debug,info,warning,error}$HUMANIZE_LOG, else warningLogging to stderr.

--check prints, on stdout, lines target, hostname, python … (pid …), one export <virtual> -> <real> per export, and workspace <path> (<n> entries).

ExitWhen
agent's ownthe agent ran
0--check succeeded
1connection, protocol, OS or value error (hmz: <why> on stderr)
2no agent and no --check (no agent given; try `hmz internal anchor claude` ), or settings no session can run under
127--native and the target has no such CLI
130interrupted

hmz internal anchor serve ​

text
hmz internal anchor serve --export VIRTUAL[:REAL] [--export …]
    (--stdio | --listen [HOST:]PORT | --peer TICKET@HOST:PORT)
    [--token TOKEN] [--log-level {debug,info,warning,error}]
OptionDefaultMeaning
--export VIRTUAL[:REAL]—Required, repeatable. A directory: VIRTUAL as the agent sees it, REAL where it is here.
--stdioServe one session over stdin/stdout.
--listen [HOST:]PORThost 127.0.0.1Serve TCP connections.
--peer TICKET@HOST:PORTServe one session through a rendezvous broker.
--token TOKEN$HUMANIZE_TOKENShared secret required from clients.
--log-level$HUMANIZE_LOG, else warning

Exactly one of --stdio, --listen, --peer.

ExitWhen
0served until the session or the process ended
1cannot listen (hmz: cannot listen on <host>:<port>: <why>), or cannot dial the peer
2a bad --export; a malformed address (hmz: malformed listen address '<a>'; expected [HOST:]PORT); a non-loopback --listen without a token (hmz: cannot listen on a non-loopback address without --token). Loopback is 127.0.0.1, ::1, localhost.

DANGER

A listener reachable from other machines runs commands for whoever holds its token. See Security.

hmz internal anchor rendezvous ​

text
hmz internal anchor rendezvous [--listen [HOST:]PORT] [--punching SECONDS]
    [--log-level {debug,info,warning,error}]
OptionDefaultMeaning
--listen [HOST:]PORT0.0.0.0:0 (every interface, any free port)Where to listen. A bare port listens on 0.0.0.0.
--punching SECONDS4.0How long two halves may try to reach each other before the broker relays their bytes.
--log-level$HUMANIZE_LOG, else info

Prints hmz internal anchor rendezvous listening <bound> <landed> on stderr once listening. Pairing is by 128-bit ticket; there is no token. Exits 2 on a malformed address, 1 when it cannot listen, 0 otherwise.

hmz internal cred ​

text
hmz internal cred (--map FROM=TO | --keep FROM=TO)... [--] COMMAND [ARGS...]

Runs COMMAND here with the syscalls naming certain paths answered from others: an account's credential files from its provider directory, and kept sessions from where Humanize keeps them.

OptionMeaning
--map FROM=TORepeatable. Answer absolute path FROM (a file, or everything under a directory) with TO; reads may be served from an in-memory copy.
--keep FROM=TORepeatable. As --map, never by copying; creates what the program writes. FROM may contain a glob in any component.
ExitWhen
the program'sit ran
1it could not be supervised (hmz internal cred: <why>); it is never run unsupervised

It exits as soon as COMMAND does. What COMMAND left running after its parent exited is not killed, and stays supervised until it exits (Providers › How a credential path is answered). | 2 | no program (no program given; try hmz internal cred --map FROM=TO -- claude``); no --map/--keep (nothing to answer with anything: give at least one --map or --keep); a malformed mapping |

hmz internal fence ​

text
hmz internal fence --policy JSON [--] COMMAND [ARGS...]

Runs COMMAND inside a fence: on Linux, Landlock for paths and, where the network is cut, a seccomp filter admitting only TCP and Unix sockets; on macOS, the whole fence as one Seatbelt profile applied by /usr/bin/sandbox-exec. Where the network is cut, a loopback proxy (HTTPS_PROXY and related, NO_PROXY emptied) passing only the fence's hosts; TMPDIR set to the fence's scratch directory.

OptionMeaning
--policy JSONRequired. The fence as JSON, or @PATH for a file holding it.
ExitWhen
the program'sit ran
2no --policy; no program (no program given; try hmz internal fence --policy=... -- claude``)
126the fence cannot be put up here (no Landlock, or no way to cut TCP; on macOS, a process already inside a sandbox); the program never ran (hmz internal fence: <why>)

hmz internal hook ​

text
hmz internal hook --at SOCKET

Reads one hook call on stdin (JSON, re-serialised onto one line), sends it followed by \n to the Unix socket SOCKET, and writes the one line answered to stdout. See Agents › Hooks.

ExitWhen
0always after reading the line: whatever the flow answered; when the socket cannot be connected (hmz internal hook: <socket>: <why> on stderr); and when it closes before answering (silently). Empty stdout lets the tool through.
1an unreadable command line. Never 2, which the CLIs read as the hook refusing the tool.

hmz internal tools ​

text
hmz internal tools --at SOCKET

Carries the tool protocol both ways at once between stdin/stdout and the Unix socket SOCKET; the end of either direction ends the other.

ExitWhen
0either end closed
1the socket cannot be reached (hmz internal tools: <socket>: <why>)
2no --at

hmz internal cli ​

text
hmz internal cli --at SOCKET --name NAME [-- WORDS...]

Sends {"name": NAME, "argv": WORDS} on one line to the Unix socket SOCKET, reads one line {"code", "out", "err"} back, writes out to stdout and err to stderr, and exits code. Everything after the first -- is passed on untouched, --help and later -- included. Stdin is not read. See Agents › Command lines.

ExitWhen
the flow'sit answered: 0, 2 for a usage error, 127 for a command line no longer shelved, or what the flow's function exited with
1the socket cannot be reached (NAME: the flow serving it has ended (<why>)), or no answer came back (NAME: the flow serving it gave no answer)
2no --at or --name

Exit statuses ​

StatusCommandMeaning
0allSuccess. For hmz exec, including a run its budget stopped.
1hmzRuns held by an older Humanize; or the host let go of the interface.
1hmz webRuns held by an older Humanize; or the runs or the port could not be had.
1hmz updateInstalled from a source, or no installer, PyPI or install that worked.
1hmz execThe flow raised (traceback on stderr; the crash is reported where reporting is on).
1internal …Could not connect, listen, supervise, or reach a socket.
2allThe line was wrong: argparse rejections and everything in What is refused.
126internal fenceThe fence could not be put up; the program never ran.
127internal anchor --nativeThe target has no such CLI.
130hmz exec, internal anchorInterrupted (SIGINT); for hmz exec, after the run has let go of what it made.
129, 143hmz execSIGHUP, SIGTERM, after the run has let go of what it made.
program'sinternal anchor, cred, fenceThe wrapped program's own status.
flow'sinternal cliWhat the flow answered the run with.

Environment variables ​

Variables these commands read. The complete list, with every layer's, is Environment variables.

VariableRead byValuesEffect
HUMANIZE_HOMEalla pathWhere Humanize keeps what outlives a run. Default ~/.hmz, which a ~/.humanize is moved to. Empty is unset. Not created until written.
HUMANIZE_DAEMONhmz, hmz weboff, 0, no (case-insensitive, stripped)Hold runs in the interface's process. Anything else, empty included, holds them apart.
HUMANIZE_NAMEhmz, SDK linksa nameThe name a frontend attaches under, before @<kind>. Default: the login name.
HUMANIZE_SENTRYhmz, hmz execon, offAnswers reporting for this process without writing the answer down.
HUMANIZE_RELEASEShmz, hmz web, hmz updatea URL, a path, or off, 0, no, none, emptyWhere the newest release is read from (PyPI's JSON for hmz by default); off, nothing asks.
NO_COLOR, TERM, FORCE_COLORallsee ColourEscapes.
TEXTUAL_THEMEhmza Textual theme nameThe interface's theme; unknown names are ignored.
TMUX, TERM_PROGRAM, LC_TERMINALhmzDecide TEXTUAL_DISABLE_KITTY_KEY (Terminal preparation).
HUMANIZE_TARGETinternal anchora target URLDefault --target.
HUMANIZE_HARNESSinternal anchora harness spellingDefault --harness.
HUMANIZE_SHADOWinternal anchora pathDefault --shadow.
HUMANIZE_RENDEZVOUSinternal anchora hostDefault --broker.
HUMANIZE_TOKENinternal anchor, anchor servea secretDefault --token.
HUMANIZE_LOGinternal anchor, serve, rendezvousdebug info warning error (case-insensitive, stripped)Default --log-level. Any other value is ignored with hmz: ignoring HUMANIZE_LOG='<value>', which is not one of debug, info, warning, error on stderr.

Set by hmz internal anchor inside the agent it runs: HUMANIZE (the launching half's version), HUMANIZE_TARGET, HUMANIZE_WORKSPACE.

Files ​

Paths these commands read or write, under $HUMANIZE_HOME (~/.hmz). The whole layout is Files.

PathCommandAccess
epics/<workspace>/<datetime>-<hex>/hmz exec, runs started from hmzwritten: one epic per run
settings.yamlhmzread and written: what the interface remembers; its runtimes, fallbacks and clis are read by all when an agent or environment is opened
history.jsonlhmzread and written: prompt history
epics/<workspace>/<datetime>-<hex>/host.loghmzappended: what the host process wrote while it held that run. The daemon's socket, record, lock and log are in the machine's temporary directory, not here
prices.jsonallmodel prices, refreshed when older than a day by the interface as it opens and by a run as it starts
releases.json (in the machine's directory)hmz, hmz web, hmz updatethe newest release, refreshed when older than a day by the interfaces as they open and by hmz update
settings.yaml, providers/allread when an agent or environment is opened
flowverses/; ~/.hmz/flows/ and ./.hmz/flows/ (fixed paths, not moved by HUMANIZE_HOME)allread when -f is resolved

Python equivalents ​

Every command is a shell around hmz.sdk.Hmz:

Command linePython
hmz exec <argv>Hmz().exec(argv)
reading an hmz exec lineHmz().read(argv) → Line
hmz exec without the lineHmz().run(flow, task, agents=…, envs=…, params=…, budget=…, profile_flow=…, profile_environment=…, resume=…)
hmz (as a frontend of held runs)Daemons().host().link()

hmz web is a frontend of held runs as hmz is; in Python it is hmz.web.serve(), which is not part of the SDK. See Web › Module.

Released under the Apache-2.0 licence.