Skip to content

Viewing Logs

A run writes everything it knows to one file — one JSON object per line — and prints almost nothing to the console. wardcell logs reads those files.

One file per run, named by the run id:

macOS~/Library/Logs/wardcell/runs/<run_id>.jsonl
Linux$XDG_STATE_HOME/wardcell/runs/<run_id>.jsonl, else ~/.local/state/wardcell/runs/<run_id>.jsonl

wardcell doctor prints the directory.

A few lines before the sandboxed child takes over — the run id, the log path, and a live-tail hint — then one summary line after it exits. Between the two wardcell writes nothing, so a TUI agent in the sandbox (Claude Code, an editor) is never corrupted.

If the run blocked any network requests, the summary names each destination with a count and the exact DSL line that would allow it:

wardcell: run 0192f0a1 exited 0 in 4m12s
wardcell: 3 network request(s) blocked, 2 destination(s):
api.openai.com:443 ×2 allow with: net allow_http "api.openai.com"
telemetry.acme.io:443 ×1 allow with: net allow_http "telemetry.acme.io"
wardcell: full log → wardcell logs show 0192f0a1

Everything else — every request, every grant, every decision — is in the log.

Terminal window
wardcell logs list [--all] [--status running|exited|crashed] [--output text|pretty|json]

A table of runs, newest first:

RUN ID STATUS STARTED DURATION BACKEND COMMAND
0192f0a1 running 2026-10-08 14:22 — seatbelt claude
0192efb3 exited 2026-10-08 13:05 4m12s seatbelt make test
0192ee90 crashed 2026-10-08 11:48 12s seatbelt cargo build

By default it shows runs still running plus those from the last 24 hours; --all shows every retained run. --status keeps one kind:

Status
runningthe pid is alive and no exit line has been written
exitedthe run ended cleanly and wrote its exit
crashedthe pid is gone but no exit line was written

RUN ID is a short prefix. A unique prefix is accepted anywhere a full id is, like a git short hash.

Terminal window
wardcell logs show <run-id> [-f|--follow] [--category <c>] [--level <l>] \
[--source <s>] [--since <dur>] [-n <count>] [--output pretty|json]

Views or tails one run’s log, pretty-printed by default for a live run and an ended one alike. <run-id> is a full id or a unique prefix; omit it and, when exactly one run is running, that one is used — otherwise it errors and tells you to run wardcell logs list.

Flag
-f, --followtail the log, stopping when the run exits
--category <c>one of run net fs exec proxy zone audit diag
--level <l>trace debug info warn error, and above
--source <s>cli proxy backend supervisor plugin mitm
--since <dur>a relative duration: 30s, 10m, 2h, 1d
-n <count>the last <count> events
--output pretty|jsonpretty is the default; json is the matching JSONL, untouched

A network denial renders as a loud red BLOCK badge with the unblock hint on the next line; a filesystem or exec denial uses the same badge in a quieter colour. --output json passes the matching JSONL straight through, for piping to jq.

Terminal window
# watch a run's network decisions as they happen
wardcell logs show 0192f0a1 -f --category net
# the proxy's per-request firehose, once logs.file_min_level = "trace"
wardcell logs show 0192f0a1 --source mitm
Terminal window
wardcell logs prune

Runs the retention sweep now and reports how many logs it removed.

One JSON object per line:

Field
tsRFC3339, millisecond precision, UTC
runthe run id, 32 lowercase hex characters
leveltrace|debug|info|warn|error
sourcecli|proxy|backend|supervisor|plugin|mitm
categoryrun|net|fs|exec|proxy|zone|audit|diag
zonepresent only on a multi-zone run
eventa dotted discriminator — run.started, run.environment, run.exited, net.denied, net.muted, net.approval_requested, net.approved, net.upstream_error, net.credential_unresolved, fs.denied, audit.decision, audit.completed, exec.launch
msga human one-liner
dataan event-specific object of typed fields

run.started is the first line of every run’s log — it carries pid, argv, cwd, backend and fragments, and is written before anything else so a run is always discoverable. run.environment follows with vars: every environment variable injected into the sandbox, as one object. net.denied carries operation, host, port and reason. A host the policy muted with mute_hosts is still blocked, but its denials are logged as net.muted at debug rather than net.denied at warn — so an expected, noisy endpoint stays out of the default log while -vv can still audit what was silenced. When a network-approval plugin is configured, a host or request no grant covers is asked about rather than blocked: net.approval_requested records the question (host, port, and method/path when there is one) and net.approved records an allow (with its scope — once or always — its applies_to for an always, and, when the plugin gave them, by and a reference); a refusal is the ordinary net.denied. run.exited carries exit_code, exit_source (command or wardcell) and duration_ms. The structural records (run.started, run.environment, run.exited) are always written, whatever logs.file_min_level is set to.

{"ts":"2026-10-08T14:22:01.512Z","run":"0192f0a1c4e37b2d9f6a1e5c8b3d4f00","level":"info","source":"cli","category":"run","event":"run.started","msg":"run started: make","data":{"pid":40192,"argv":["make","test"],"cwd":"/work","backend":"seatbelt","fragments":["base","toolchains/rust"]}}
{"ts":"2026-10-08T14:22:01.514Z","run":"0192f0a1c4e37b2d9f6a1e5c8b3d4f00","level":"info","source":"cli","category":"run","event":"run.environment","msg":"sandbox environment","data":{"vars":{"PATH":"/usr/bin:/bin","HOME":"/work","HTTP_PROXY":"http://127.0.0.1:51823"}}}
{"ts":"2026-10-08T14:22:03.908Z","run":"0192f0a1c4e37b2d9f6a1e5c8b3d4f00","level":"warn","source":"proxy","category":"net","event":"net.denied","msg":"blocked api.openai.com:443","data":{"operation":"network-outbound","host":"api.openai.com","port":443,"reason":"no allow_http rule matches"}}
{"ts":"2026-10-08T14:26:13.004Z","run":"0192f0a1c4e37b2d9f6a1e5c8b3d4f00","level":"info","source":"cli","category":"run","event":"run.exited","msg":"run exited 0","data":{"exit_code":0,"exit_source":"command","duration_ms":252000}}

--output json is the matching JSONL on stdout, so jq does the rest:

Terminal window
# every blocked host
wardcell logs show 0192f0a1 --output json | jq -r 'select(.event=="net.denied") | .data.host'
# the environment the sandbox actually received
wardcell logs show 0192f0a1 --output json | jq 'select(.event=="run.environment") | .data.vars'

Read runs/<id>.jsonl directly for the same stream when wardcell is not on hand.

Old run logs are pruned on each run start, and on demand by wardcell logs prune. The defaults keep 14 days and at most 200 runs, whichever is stricter; a run still running is never pruned. Both limits are [logs] settings.