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 'log("hello")'
harn run --deny shell,exec <file.harn>
harn run --allow read_file,write_file <file.harn>
harn run --no-sandbox <file.harn>
harn run --read-only-root /path/to/other-repo main.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) |
--no-sandbox | Disable the default worktree filesystem/process sandbox and network side-effect ceiling |
--read-only-root <path> | Read from an extra filesystem root while keeping sandboxing enabled |
--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 |
--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 } — emitted once when harn run <bundle.harnpack> resolves a pack to execute. |
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 five-phase 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: 1):
{
"schema_version": 1,
"event": "run_phase",
"phases": [
{ "name": "parse", "duration_ms": 12, "input_bytes": 4096 },
{ "name": "typecheck", "duration_ms": 80 },
{ "name": "bytecode_compile", "duration_ms": 35, "cache": "miss" },
{ "name": "run_setup", "duration_ms": 8 },
{ "name": "run_main", "duration_ms": 1200, "events": 14 }
]
}
The phase array is always in this order: parse, typecheck,
bytecode_compile, run_setup, run_main. On a bytecode-cache hit,
parse and typecheck stay present with duration_ms: 0, and the
bytecode_compile row flips to "cache": "hit".
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
--read-only-root when --no-sandbox is present.
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(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