CLI reference
All commands available in the harn CLI.
To add a new subcommand or port an existing one off Rust, see
Extending the CLI in .harn. For the
machine-readable side of --json modes, see the
harn --json contract.
harn run#
Execute a .harn file.
harn run <file.harn>
harn run --trace <file.harn>
harn run --profile --profile-json profile.json <file.harn>
harn run --flight-recorder <file.harn>
harn run -e 'harness.stdio.log("hello")'
harn run --deny shell,exec <file.harn>
harn run --allow read_file,write_file <file.harn>
harn run --standalone --allow json_parse policy.harn
harn run --approve-risky git.push release.harn
harn run --no-sandbox <file.harn>
harn run --write-root /path/to/output main.harn
harn run --read-only-root /path/to/other-repo main.harn
harn run --sandbox-write-root /path/to/tool-cache main.harn
harn run --sandbox-read-root /path/to/sdk main.harn
harn run --grant gh_token=secret://gh/token,expose=GH_TOKEN,for=gh open_pr.harn
harn run --environment-policy isolated eval_child.harn
harn run --yes <file.harn>
harn run --explain-cost <file.harn>
harn run --attest <file.harn>
harn run --attest --receipt-out receipt.json <file.harn>
harn run <bundle.harnpack>
harn run --dry-run-verify <bundle.harnpack>
harn run --allow-unsigned <bundle.harnpack>
harn run --resume .harn/workers/worker_...json
| Flag | Description |
|---|---|
--trace | Print LLM trace summary after execution. Can also be set with HARN_TRACE=1 |
--flight-recorder | Save the bounded, value-free VM code path under .harn-runs/flight-recordings/. Can also be set with HARN_FLIGHT_RECORDER=1. |
--flight-recorder-out <path> | Write the flight recording to one explicit file. Requires --flight-recorder; Harn doesn't rotate files in a caller-owned directory. |
--flight-recorder-max-events <count> | Retain the newest 1 to 1,000,000 VM events in memory. Default: 250,000. Requires --flight-recorder. |
--flight-recorder-retain <count> | Retain the newest 1 to 1,024 recordings in Harn's default directory. Default: 16. Requires --flight-recorder. |
--profile | Print a categorical timing breakdown after execution. Can also be set with HARN_PROFILE=1 |
--profile-json <path> | Write the categorical timing breakdown as JSON. Can also be set with HARN_PROFILE_JSON=<path> |
--explain-cost | Print static LLM token/cost estimates without executing the script |
-e <code> | Evaluate inline code instead of a file |
--resume <handle-or-snapshot> | Cold-restore a suspended top-level agent from its persisted worker snapshot |
--deny <builtins> | Deny specific builtins (comma-separated) |
--allow <builtins> | Allow only specific builtins (comma-separated) |
--eager-project-handlers | Load every project trigger and hook handler module during startup |
--project-triggers | Register manifest triggers and reconcile their durable state for this run |
--standalone | Run without loading ambient project configuration, skills, handlers, state roots, or manifest-derived authority |
--approve-risky <operation> | Explicitly authorize one exact risky stdlib operation for this invocation; repeatable (for example git.push) |
--no-sandbox | Disable the default worktree filesystem/process sandbox and network side-effect ceiling |
--allow-process-loopback | Allow confined child processes to serve and connect over IPv4 or IPv6 loopback without opening remote egress. Supported on macOS; other local sandbox backends fail closed. |
--allow-process-network | Allow child network while retaining filesystem/process confinement. On macOS, child traffic stays denied until HARN_EGRESS_* or harness.net.egress_policy(...) configures an allow decision through the managed proxy. On other local platforms Harn does not attach that proxy; the grant raises the capability ceiling and children have unrestricted sockets. See Managed child-process egress. |
--write-root <path> | Write to an extra filesystem root while keeping sandboxing enabled |
--read-only-root <path> | Read from an extra filesystem root while keeping sandboxing enabled |
--sandbox-write-root <path> | Let spawned subprocesses write an extra root without granting Harn filesystem builtins access |
--sandbox-read-root <path> | Let spawned subprocesses read an extra root without granting Harn filesystem builtins access |
--sandbox-unix-socket-root <path> | Let spawned subprocesses bind and connect Unix-domain sockets whose socket file lives under this root, without opening any IP networking. Build servers (sbt, Gradle, MSBuild) need this. Supported on macOS; other local sandbox backends fail closed. |
--environment-policy <inherited|isolated|granted> | Choose which launch-time environment values this session and its subprocesses may read. Default: inherited. |
--grant <spec> | Grant one named value to this session; repeatable. Selects granted when the policy is omitted. See Environment policies and grants. |
--yes | Accept first-run provider setup prompts, including local Ollama config seeding |
--attest | Emit a signed provenance receipt after execution |
--receipt-out <path> | Write the receipt to a specific JSON path |
--attest-agent <id> | Agent id used to load or create the Ed25519 signing key |
--json | Emit a versioned NDJSON event stream on stdout instead of mixed pipeline output |
--quiet | When --json is set, drop stdout and stderr events (transcript/tool/hook/persona/result still flow) |
--emit-summary-json | Emit one terminal run_summary JSON object as a single NDJSON line; defaults to stderr |
--summary-file <path> | Write --emit-summary-json output to a file instead of stderr |
--summary-fd <fd> | Write --emit-summary-json output to an already-open Unix file descriptor |
--emit-phase-json | Emit one terminal run_phase JSON object as a single NDJSON line; defaults to stderr |
--phase-file <path> | Write --emit-phase-json output to a file instead of stderr |
--phase-fd <fd> | Write --emit-phase-json output to an already-open Unix file descriptor |
--emit-rusage-json | Emit one terminal run_rusage JSON object as a single NDJSON line; defaults to stderr |
--rusage-file <path> | Write --emit-rusage-json output to a file instead of stderr |
--rusage-fd <fd> | Write --emit-rusage-json output to an already-open Unix file descriptor |
--allow-unsigned | When running a .harnpack, accept bundles that carry no Ed25519 signature (local-dev override) |
--dry-run-verify | When running a .harnpack, verify the signature and replay into the cache without executing the entrypoint |
Every completed VM invocation through harn run writes an automatic
.harn-runs/hxe-....json run record. Harn retains the newest 128 automatic
records without deleting workflow or imported records. The record's
evidence.execution_id is the same identity attached to its spans and optional
flight recording. --json emits evidence_persisted after this record is
readable and before the terminal result or error event.
--approve-risky is explicit operator authority, not pipeline configuration. It
records a receipt on the protected operation and never relaxes the generic
process.exec safety floor or configured command-policy rules.
Project handler startup#
harn run installs project hooks by default so policy remains fail-closed, but
does not register manifest triggers or reconcile their durable state unless the
run passes --project-triggers. Trigger-oriented commands continue to register
the triggers they operate. This makes durable project side effects explicit for
ordinary entrypoint runs.
With --project-triggers, Harn validates trigger declarations, handler exports,
callable signatures, and predicate signatures before it runs the entry script.
Trigger modules and imports initialize on matching dispatch. Project policy
hooks validate their declaration shapes at startup but defer export, signature,
module, and import resolution until matching dispatch. A matching policy hook
that cannot be resolved fails closed; an unrelated broken hook stays off the
startup path.
Use --eager-project-handlers to diagnose failures in top-level handler module
initialization. It restores fail-fast initialization for every project handler
and implies --project-triggers.
Embedded callers select the same contract with
commands::run::RunExecutionOptions::project_runtime. Use
ProjectRuntimeMode::WithTriggers for lazy trigger dispatch or
ProjectRuntimeMode::EagerHandlers for fail-fast handler initialization; the
default Project mode keeps hooks active without reconciling durable trigger
state. The JSON execution API has the matching execute_run_json_with_options
entrypoint. Every in-process entrypoint owns a poll-scoped trigger registry;
manifest bindings created by one run are discarded at its boundary while the
embedding caller's prior registry is restored unchanged.
Use --standalone for latency-sensitive utilities and policy scripts that must
behave the same regardless of their containing directory. Relative and standard
library imports still work, and explicit CLI limits such as --allow, --deny,
and sandbox roots still apply. Standalone mode does not load a surrounding
harn.toml, project skills, project handlers, project state roots, or
manifest-derived trusted-host authority. It conflicts with commands whose
meaning requires project state, including --eager-project-handlers,
--project-triggers, --resume, and --as-job.
Environment policies and grants#
Every launched Harn session has one environment policy:
- Inherited (the default) captures the launcher's environment once when the session starts. Later changes to the launcher process do not change the session.
- Isolated admits only the small set of operating-system and toolchain
values needed to run commands, such as
PATH, temporary-directory settings, locale, and compiler locations. It rejects grants. - Granted starts with the same runtime essentials as
isolated, then adds only the declared grants.
The policy applies consistently to harness.env, Harn's provider credentials,
provider base URLs and regions, Azure/Vertex/Bedrock configuration, and spawned
commands. In isolated and granted, platform SDKs cannot silently fall back
to home-directory profiles, metadata services, or application-default
credentials. Supply the required values as grants or choose inherited.
This policy governs environment variables only. It is a different boundary
from the filesystem and process sandbox, which decides which directories a run
may touch and what a spawned command may do. See
Process sandboxing for that one. Neither boundary widens the
other: --no-sandbox leaves the environment policy fully in force, and an
environment grant gives no file, network, or tool access. Approval policy is a
third, separate thing again.
Each --grant is NAME=SOURCE[,expose=ENV_VAR][,for=COMMAND]:
| Part | Meaning |
|---|---|
NAME | A unique, non-secret name used in receipts and diagnostics. |
SOURCE | env:VAR_NAME snapshots that launcher variable at session launch. secret://ACCOUNT/KEY keeps a live secret-store reference, so rotation and revocation take effect without restarting the session. |
,expose=ENV_VAR | Optional. Makes the value available under this unique environment name. Without ,for=, the exposure is session-scoped: harness.env, provider configuration, and every spawned command. |
,for=COMMAND | Optional. Requires ,expose=. Binds the exposed variable to spawns whose executable basename matches COMMAND (for example gh for /usr/bin/gh). Command-bound grants are invisible in-process — Harn's own harness.llm.call is not an exec — so provider keys stay session-scoped by omitting ,for=. |
# Let only `gh` see a vault-backed token; other process.exec calls do not inherit it.
harn run --grant gh_token=secret://gh/token,expose=GH_TOKEN,for=gh open_pr.harn
# Snapshot a provider key from the launcher env, exposed under the same name
# for this run's model calls and every spawned command.
harn run --grant fireworks=env:FIREWORKS_API_KEY,expose=FIREWORKS_API_KEY agent.harn
Duplicate grant names and duplicate expose targets are launch errors. A child
session inherits its parent's resolved environment by default. It may narrow
that authority (for example, inherited to isolated, or a smaller subset of
granted grants), but it cannot widen it or reread the ambient host
environment. Ceiling violations return the stable
environment_policy.child_exceeds_parent code with the parent and requested
policies.
Here “reproducible” has a precise scope: every prompt, worker, and subprocess
in one launched session uses the same launch-time environment snapshot,
independent of later launcher mutations. Command-bound grants
(,for=COMMAND) still draw from that snapshot; they only narrow which
spawns receive an exposed variable. The launcher/host establishes the
snapshot, and Harn enforces it. This does not claim that two machines have
identical environments. Use isolated or explicit grants when a test or
replay must not depend on the operator's shell. A live secret:// source may
intentionally change after rotation or revocation; its receipt stays stable,
but its value is not frozen.
An .env file is not a second policy channel. A launcher may load it before
starting Harn; then its values are ordinary launcher environment values and
follow the selected policy. A vault or keyring remains the source behind a
secret:// grant. Filesystem access to service-account files or other
credential files is governed separately by the sandbox.
Process-bootstrap and host-ceiling settings that Harn must read before or above
any session—such as the path to a provider catalog overlay, server
observability controls, global rate limits, and the host's
HARN_LLM_CALLS_DISABLED kill switch—belong to the launcher process and may
require a restart to change. A child policy cannot hide or override them. They
are not session values.
Every declared grant is disclosed on stderr at launch (its name, source kind,
and exact environment target, if any—never the value) and recorded as a
non-secret receipt in the --attest provenance log.
The disclosure names only a departure from the launcher's own environment.
isolated and granted always announce themselves. The default inherited
policy with no grants prints nothing, so a plain harn run leaves stderr clean
for --summary-fd and --rusage-fd consumers. The sandbox delta line described
in Sandboxing follows the same rule.
Both --environment-policy and --grant are also accepted by
harn time run, which shares harn run's confinement surface.
Terminology:
- Launcher: the process that starts a Harn session, such as the
harnCLI or an ACP host. - Session: one conversation/execution context and its delegated lineage.
ACP defines the session lifecycle;
environmentPolicyis a Harn extension. - Worker: delegated work inside that lineage. It receives no more environment authority than its parent.
- Subprocess: an operating-system command started by the session. It sees the same resolved session environment plus explicit per-call overrides.
- Grant: a named, receipted source-to-environment mapping. Grants are session-wide today.
- Sandbox: the separate file/process/network boundary.
- Approval: permission for a risky operation; it does not add environment values.
Exit codes#
A Harn program chooses its own exit status — return 42 and
harness.runtime.exit(42) both produce 42, clamped to 0..=255. So no status
is unreachable by a program that insists on returning it. Two are nonetheless
reserved: Harn documents them, never produces them for a program outcome, and a
caller may branch on them.
| Status | Meaning |
|---|---|
0 | The program ran and succeeded. |
1 | The program ran and failed, or did not compile. |
124 | Reserved. The run was interrupted or hit a deadline. |
125 | Reserved. Harn could not prepare the run. The program never started. |
125 covers everything Harn does on the program's behalf before handing it
control: materializing locked dependencies, installing manifest triggers and
hooks, loading provider connectors, and launching the session's environment
policy. Without it, a harness that shells out to Harn cannot tell "your
dependencies could not be prepared" from "your code returned a failure" — a
distinction that decides whether to retry, to report an infrastructure fault, or
to surface a real result.
Two things that happen before the program runs are deliberately not setup
failures, and keep status 1:
- A compile error. The program's own content failing is the program failing.
- A
.harnpackverification refusal — an absent signature, or one that does not cover the bytes on disk. That is a verdict on the artifact, not a fault in the host that was asked to run it, and a caller reading125as "retry the infrastructure" would retry a tampered bundle forever.
harn test uses the same reserved statuses. It materializes the suite's
dependencies once before any case runs, so a lock that cannot be prepared exits
125 instead of reporting every case as a failed assertion.
Under --json the terminal error event's code names the phase
(package_materialization, manifest_triggers, manifest_hooks,
manifest_connectors, entry_unreadable, compile_error, …), so a consumer
that wants more than the status does not have to parse prose for it.
--json event stream#
harn run --json <file> writes one JsonEnvelope per line to stdout,
each carrying a typed RunEvent. The stream is strictly ordered via a
monotonic seq (starts at 1) so a downstream agent can reconstruct
the run without parsing prose. The envelope's schemaVersion is 1;
see harn --json-schemas for the catalog entry.
Event types (the event_type discriminator lives at
data.event_type):
event_type | Payload |
|---|---|
stdout | { payload: string } — bytes written via print/println/log, verbatim. |
stderr | { payload: string } — bytes written via eprint/eprintln. |
transcript | { agent_id?: string, kind: string, payload: object } — one entry from the LLM-call transcript stream. |
tool_call | { call_id, name, args, started_at } — model-issued tool invocation. |
tool_result | { call_id, ok: bool, result } — outcome of a tool invocation. |
hook | { name, phase, payload? } — session lifecycle hook fired during the run. |
persona_stage | { persona, stage, transition } — persona-stage transition (started / completed / handoff_started / …). |
pack_run | { bundle_hash, signature_verified: bool, key_id?: string, cache_hit: bool, dry_run_verify: bool, execution_artifact_state, artifact_fallback_reason?, artifact_decode_ms } — emitted once when harn run <bundle.harnpack> resolves a pack to execute. execution_artifact_state is linked, legacy_v2, or source_fallback. |
result | { value, exit_code: int } — terminal event for successful runs. |
error | { error: { code, message, details? } } — terminal event when a fatal error prevents a result. |
The stream is line-flushed per event. Streaming consumers can pipe
through jq -c . for live filtering:
harn run --json examples/hello.harn | jq -c '.data | {seq, event_type}'
Post-run summary JSON#
harn run --emit-summary-json <file> emits one raw JSON object after
the run finishes. This is a separate opt-in sink from harn run --json
so consumers that need aggregate metrics can keep the event stream
contract unchanged. By default the line is appended to stderr after
human diagnostics; use --summary-file <path> or --summary-fd <fd>
to isolate it from the script's own stderr.
Shape (schema_version: 4):
{
"schema_version": 4,
"event": "run_summary",
"wall_time_ms": 1234,
"exit_code": 0,
"llm": {
"call_count": 2,
"provider_call_count": 3,
"input_tokens": 1024,
"output_tokens": 256,
"time_ms": 480,
"cost_usd": 0.0042,
"known_cost_usd": 0.0042,
"unpriced_calls": 0,
"usage_unknown_calls": 0
},
"profile": {
"total_wall_ms": 1234,
"by_kind": [],
"residual_ms": 12,
"top_llm_calls": [],
"top_tool_calls": [],
"steps": []
}
}
profile is present only when the run enables profiling with
--profile or --profile-json. The LLM metrics are collected whenever
summary JSON is requested, even without --trace.
llm.call_count is the logical-operation count, while
llm.provider_call_count includes physical retries. Exact cost_usd becomes
null if any physical call is unpriced; known_cost_usd remains the explicit
lower bound. A nonzero usage_unknown_calls means the reported token/cache
totals are also lower bounds rather than known-complete usage.
Post-run phase JSON#
harn run --emit-phase-json <file> emits one raw JSON object after
the run finishes. This uses the same fixed seven-row contract as
harn time run --json, but keeps the data on a separate sink so a
wrapper can spawn harn run and recover parse/typecheck/compile/setup
timings without changing stdout. Use --phase-file <path> or
--phase-fd <fd> to isolate the line from stderr.
Shape (schema_version: 2):
{
"schema_version": 2,
"event": "run_phase",
"phases": [
{ "name": "parse", "kind": "top_level", "duration_ms": 12, "input_bytes": 4096 },
{ "name": "typecheck", "kind": "top_level", "duration_ms": 80 },
{ "name": "bytecode_compile", "kind": "top_level", "duration_ms": 35, "cache": "miss" },
{ "name": "run_setup", "kind": "top_level", "duration_ms": 8 },
{ "name": "run_main", "kind": "top_level", "duration_ms": 1200, "events": 14 },
{ "name": "module_compile", "kind": "attribution", "duration_ms": 40, "events": 3 },
{ "name": "module_load", "kind": "attribution", "duration_ms": 75, "events": 8 }
]
}
The phase array is always in this order: parse, typecheck,
bytecode_compile, run_setup, run_main, module_compile, module_load.
On a bytecode-cache hit,
parse and typecheck stay present with duration_ms: 0, and the
bytecode_compile row flips to "cache": "hit". The final two attribution
rows overlap setup/main and are not additive.
Post-run rusage JSON#
harn run --emit-rusage-json <file> emits one raw JSON object after
the run finishes containing the total getrusage(RUSAGE_SELF) CPU time
sample for the process during that run. Use --rusage-file <path> or
--rusage-fd <fd> to isolate the line from stderr.
Shape (schema_version: 1):
{
"schema_version": 1,
"event": "run_rusage",
"cpu_ms": 320
}
You can also run a file directly without the run subcommand:
harn main.harn
By default, harn run installs a worktree sandbox before executing
the VM. Filesystem and subprocess cwd access are rooted at the nearest
harn.toml project root, or at the invocation working directory when
no project manifest is present. Network side effects are denied by the
same default policy. Use --no-sandbox only for scripts that need the
old unrestricted process behavior; use --read-only-root when a script
needs read access to a sibling or shared directory and should remain in
the sandboxed profile.
The CLI emits a warning when --no-sandbox is used, and rejects
--write-root / --read-only-root when --no-sandbox is present — a
scoped grant is meaningless once the whole sandbox is off. When a grant
is used with the sandbox still active, the CLI prints one line naming
exactly the delta (for example sandbox active; extra write root: /path) instead of the blanket --no-sandbox banner, so a routine
grant-scoped run does not spam a filesystem/process/egress warning.
Before starting the VM, harn run <file> builds the cross-module
graph for the entry file. When all imports resolve, unknown call
targets produce a static error and the VM is never started — the same
call target ... is not defined or imported message you see from
harn check.
The inline -e <code> form is wrapped in pipeline main(harness: Harness, task) { ... }
and run as a temp file in the current directory, so:
- Leading
import "..."(andpub import { ... } from "...") lines are hoisted out of the wrapper. They must come first; imports that appear after another statement are not lifted. - Relative imports resolve against the working directory:
harn run -e