Skip to the content.

Debugging guide: why did fak do that to my call?

A tool call came back denied, repaired, or quarantined and you want to know why — which of the eight folded rungs decided it, what reason it cited, and what would make it pass. fak answers this from evidence at every layer, so you never have to read kernel source or guess. This runbook is the loop, from the fastest offline check to a full post-hoc session replay.

The companion observability guide is the reference for the metrics, logs, and trace_id surfaces. This page is the task: diagnosing one surprising verdict, end to end. Every surface below is designed to never log a request body, a tool argument, or result content — only digests and bounded disclosures.


1. Reproduce it offline, deterministically — fak preflight --explain

The capability floor is the same code whether a model is in the loop or not, so you can reproduce any verdict with no key, no model, and no gateway. The default one-liner tells you what; --explain tells you why — every rung that folded, what each returned, and which one won:

fak preflight --tool write_file --args '{"path":"internal/abi/x.go","content":"…"}' \
  --policy your-policy.json --explain
tool: write_file   args: 54 bytes (sha 8b3b16b5d7c8)
verdict: DENY   reason: SELF_MODIFY   by: monitor   disposition: ESCALATE
witness: internal/abi/
explanation: write_file denied by monitor: SELF_MODIFY (ESCALATE). offending set: internal/abi/.

decision chain (8 rung(s), most-restrictive wins):
   [0] grammar.Rung               DEFER     by=grammar
   [1] ratelimit.Limiter          DEFER     by=ratelimit
   [2] preflight.Ladder           DEFER     by=preflight
   [3] engine.residencyGate       DEFER     by=engine-residency
   [4] plancfi.Adjudicator        DEFER     by=plancfi
   [5] ifc.SinkGate               DEFER     by=ifc-sink
   [6] shipgate.ShipAdjudicator   DEFER     by=shipgate
=> [7] adjudicator.Adjudicator    DENY      SELF_MODIFY by=monitor   {internal/abi/}   <- winner (rank 100)

This is the single most useful command in the box. It tells you:

For tooling (or a debugging skill), --json emits the same Decision structured and safe to log — it carries an args digest, never the raw args:

fak preflight --tool refund_payment --args '{}' --policy your-policy.json --json

A TRANSFORM verdict additionally lists the redacted arg keys it rewrote; an admit-and-log posture surfaces posture and would_deny.


2. Find the call in a live fak serve

Over the wire, every adjudication rides back inline on the response — fak on /v1/chat/completions, _fak on /v1/messages — with the verdict, the deciding by, the reason, and the trace_id:

"_fak": { "version": "fak/v1", "admissions": [
  { "tool": "Bash", "verdict": "DENY", "reason": "POLICY_BLOCK", "by": "monitor", "trace_id": "…" }
]}

That same trace_id is returned in the X-Trace-Id response header and stamped on the structured access log. To follow one request across every surface, filter the access log by it — the per-operation line now names who decided (by):

# the structured op log is always on (not gated behind the audit journal)
jq 'select(.event=="gateway_operation" and .trace_id=="<id>")' fak-serve.log
# -> {"operation":"adjudicate","tool":"Bash","verdict":"DENY","reason":"POLICY_BLOCK","by":"monitor","disposition":"TERMINAL", …}

3. See the pattern across the fleet — /metrics

When the question is “why is my deny rate / miss rate high across the workload”, the Prometheus surface answers in aggregate. Two families added for exactly this:

# WHO is refusing (which rung), not just that something was refused:
sum by (by, reason) (fak_gateway_operations_total{verdict="DENY"})

# WHY the vDSO fast path isn't hitting — each miss attributed to a cause instead of a
# bare ok=false (DESTRUCTIVE / MISSING_HINTS / RESOURCE_MISNAMED / WITNESS_REVOKED / NOT_CACHED):
sum by (reason) (fak_vdso_misses_total)

A high fak_vdso_misses_total{reason="MISSING_HINTS"} means your client is dropping the readOnlyHint/idempotentHint metadata; a high DESTRUCTIVE means the tools simply aren’t fast-path eligible. The full metric catalog is in the observability guide.


4. Pull the durable record — GET /v1/fak/events

With the audit journal enabled (FAK_AUDIT_JOURNAL=/path/to/journal.jsonl), every decision is a hash-chained, append-only row you can drain after the fact:

curl -s 'http://127.0.0.1:8080/v1/fak/events?since=0' | jq '.'

Each row carries the verdict, reason, by, and content digests, plus two fields for correlation and forensics:


5. Replay a finished session — fak debug

To diagnose a whole Claude Code session after the fact, attach to its transcript as a core image. You don’t need to remember where transcripts live — discover them:

fak debug --list
# found 42 Claude Code transcript(s); most recent first:
#   [ 1] 2026-06-22 01:56     1.1M  7ef0a89e-….jsonl
#        fak debug --session "…/projects/…/7ef0a89e-….jsonl"

Then attach the one you want and ask a question — fak demand-pages only the working set the question touches, and drives every tool result back through the shipped gate (so a poisoned/secret result seals on ingest, exactly as it would live):

fak debug --session "<path>.jsonl" --cmd bt          # backtrace of pages, sealed ones marked
fak debug --session "<path>.jsonl" --cmd ws --query "what did the kernel quarantine?"

(With no --session, fak debug runs a hermetic demo over a committed fixture and says so — run --list to point it at your real data.)


6. Debug an agent A/B run — agent-report.json

fak agent writes a per-call decision trace into its report artifact, so a run that went wrong is debuggable from the JSON alone (no separate --log file needed). Each row in calls[] carries the arm, tool, verdict, the closed reason name and disposition on a deny, the deciding by, and a bounded args preview:

fak agent --offline --out agent-report.json
jq '.calls[] | select(.arm=="fak")' agent-report.json
# {"arm":"fak","turn":2,"tool":"fetch_policy","verdict":"DENY","reason":"TRUST_VIOLATION","by":"ifc-sink", …}

Decision vocabulary (quick reference)

Verdict Meaning Where it shows
ALLOW admitted (or vDSO-served — by=vdso) inline fak/_fak, metrics, journal
DENY refused by structure (carries a reason) everywhere
TRANSFORM args rewritten before dispatch (e.g. secret redaction) inline, --explain redacted
QUARANTINE result paged out of the model’s context journal, agent quarantines
Disposition What a loop should do
RETRYABLE the model can fix it (e.g. MALFORMED) and retry
WAIT transient (rate-limited / lease held) — back off
ESCALATE a trust/self-modify event — needs a human, not a retry
TERMINAL structurally refused — a retry will not help

Cross-references