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 -e 'harness.stdio.log("hello")'
harn run --deny shell,exec <file.harn>
harn run --allow read_file,write_file <file.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 |
--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) |
--defer-project-handlers | Parse and validate project triggers and hooks now, but load their handler code only if a handler runs |
--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-network | Allow network access for the Harn run and its child processes. Filesystem and process confinement remain active. See Network grants. |
--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 |
--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 |
--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.
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 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.
--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: 1):
{
"schema_version": 1,
"event": "run_summary",
"wall_time_ms": 1234,
"exit_code": 0,
"llm": {
"call_count": 2,
"input_tokens": 1024,
"output_tokens": 256,
"time_ms": 480,
"cost_usd": 0.0042
},
"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.
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