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}}| Key | Meaning |
|---|---|
ts | UTC, microseconds, the same format as the runtime state |
session | Claude Code session id, or null |
event / op | the hook event and the op that refused |
tool | tool events only: the tool that was denied |
reason | a short fixed sentence from the op; never prompt text |
evidence | the op’s own summary — see below |
suppressed | present 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_toolsis 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(defaulttrue) — write the line. Off: refusals still happen, nothing is recorded.nav-features disable reject_log(add--localto 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 turnsIn /nav: with d, the reads card ends with N rejects over HH:MM <op>; l opens the
last eight lines.