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.
Where a run’s log lives
Section titled “Where a run’s log lives”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.
What a run prints to the console
Section titled “What a run prints to the console”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 4m12swardcell: 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 0192f0a1Everything else — every request, every grant, every decision — is in the log.
wardcell logs list
Section titled “wardcell logs list”wardcell logs list [--all] [--status running|exited|crashed] [--output text|pretty|json]A table of runs, newest first:
RUN ID STATUS STARTED DURATION BACKEND COMMAND0192f0a1 running 2026-10-08 14:22 — seatbelt claude0192efb3 exited 2026-10-08 13:05 4m12s seatbelt make test0192ee90 crashed 2026-10-08 11:48 12s seatbelt cargo buildBy default it shows runs still running plus those from the last 24 hours;
--all shows every retained run. --status keeps one kind:
| Status | |
|---|---|
running | the pid is alive and no exit line has been written |
exited | the run ended cleanly and wrote its exit |
crashed | the 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.
wardcell logs show
Section titled “wardcell logs show”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, --follow | tail 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|json | pretty 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.
# watch a run's network decisions as they happenwardcell logs show 0192f0a1 -f --category net
# the proxy's per-request firehose, once logs.file_min_level = "trace"wardcell logs show 0192f0a1 --source mitmwardcell logs prune
Section titled “wardcell logs prune”wardcell logs pruneRuns the retention sweep now and reports how many logs it removed.
The event schema
Section titled “The event schema”One JSON object per line:
| Field | |
|---|---|
ts | RFC3339, millisecond precision, UTC |
run | the run id, 32 lowercase hex characters |
level | trace|debug|info|warn|error |
source | cli|proxy|backend|supervisor|plugin|mitm |
category | run|net|fs|exec|proxy|zone|audit|diag |
zone | present only on a multi-zone run |
event | a 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 |
msg | a human one-liner |
data | an 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}}Reading the log with jq
Section titled “Reading the log with jq”--output json is the matching JSONL on stdout, so jq does the rest:
# every blocked hostwardcell logs show 0192f0a1 --output json | jq -r 'select(.event=="net.denied") | .data.host'
# the environment the sandbox actually receivedwardcell 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.
Retention
Section titled “Retention”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.