Skip to Content
ConfigurationReject Log

Reject Log (reject_log)

Every refusal Navigator’s runtime makes, as one JSON line you can grep. The read guard denying a fifth repeated .agent/ Read, the prompt gate dropping a loop prompt that followed a skipped WORKFLOW CHECK, the stop gate forcing a continuation on an unfinished turn — each was a deterministic decision with no record beyond a tally. The 2026-10-03 stop-gate over-fire (six blocks on read-only turns) was diagnosed from the transcript. Now it is one line each.

New in v8.2.0. Ships on: the log observes and never blocks, so it does not fall under the “new blocking features seed off” rule.

The file

.agent/.nav-rejects.jsonl, gitignored (by nav-init too), newest line last:

{"ts":"2026-10-05T09:06:06.895917+00:00","session":"s1","event":"Stop","op":"stop_completion","reason":"mutating turn, 1/6 indicators met, no exit signal","evidence":{"met":1,"unmet":["code_committed","tests_passing","code_simplified","docs_updated","marker_created"],"mutating_tools":["Bash"]}} {"ts":"2026-10-05T09:12:41+00:00","session":"s1","event":"PreToolUse","op":"read_guard","tool":"Read","reason":"5 .agent/ reads this turn (escalate_threshold=5)","evidence":{"path":"tasks/TASK-88-reject-log.md","count":5,"threshold":5}}
KeyMeaning
tsUTC, microseconds, the same format as the runtime state
sessionClaude Code session id, or null
event / opthe hook event and the op that refused
tooltool events only: the tool that was denied
reasona short fixed sentence from the op; never prompt text
evidencethe op’s own summary — see below
suppressedpresent and true when the refusal was computed under PILOT_EXECUTOR and the merge belt stripped the block: the record survives, the block does not

Evidence per op:

  • read_guard — {path, count, threshold}. The path goes here, never to stderr.
  • stop_completion — {met, unmet, mutating_tools}. mutating_tools is the turn’s tools intersected with the action vocabulary, so a Bash-only over-fire reads ["Bash"].
  • prompt_gate — {trigger}: the matched loop phrase. The file never enters the model’s context, so it is stored as matched; the stderr the model sees stays redacted.

The file keeps its newest 500 lines (rewritten once it passes 600).

Config block

{ "reject_log": { "enabled": true } }
  • enabled (default true) — write the line. Off: refusals still happen, nothing is recorded. nav-features disable reject_log (add --local to keep it personal).

How it is written

A refusing op attaches reject: {reason, evidence} to its blocking result. The runtime — runtime._dispatch in Python, runOps in the mod — strips that key before the merge and appends the line from one place. Both runtimes produce identical bytes for the same refusal; a generated fixture asserts it. A failed append never reaches the op or you.

Reading it

tail .agent/.nav-rejects.jsonl grep -c '"op":"stop_completion"' .agent/.nav-rejects.jsonl grep '"mutating_tools":\["Bash"\]' .agent/.nav-rejects.jsonl # stop-gate fires on Bash-only turns

In /nav: with d, the reads card ends with N rejects over HH:MM <op>; l opens the last eight lines.