2026-10-19, Jacks-iMac. Research into what actually prevents a session from overclaiming or wasting effort, measured from 13 sessions of real gate-refusal data plus one session that produced both failures repeatedly under observation.
CLAUDE.md as a hard gate for three months and produced a 6% compliance rate. Meanwhile no override marker has ever been created on this machine: no ~/.claude/*.allow, no .opus_ok, no completion.allow_stop. When a gate refuses, the work gets fixed. The mechanism is sound, so the remaining problem is three specific defects in how the gating is wired.
| Mechanism | Measured result | Verdict |
|---|---|---|
A rule written in CLAUDE.md or memory, with nothing that runs | Batching target above 40% written since 2026-07-19; measured rate 6% across 9,009 tool turns | does not hold |
| A blocking Stop gate | Every substantive error in this session was caught by a gate, not by self-review. Overrides used: 0 | holds |
A memory file on disk with no MEMORY.md index line | The record-split bug was documented 2026-08-12, the index line never appended, and the identical bug was rebuilt and published 2026-10-19 | invisible |
| A gate proven only against positive controls | 2026-08-29: a cymbal fix passed its one positive control, shipped, then failed a negative control already on disk | unsafe |
| A gate proven in both directions before wiring | Four hooks built today at 5, 14, 10 and 11 of their controls, two of which are fixtures that must stay quiet | holds |
_stop_budget.py records that on one event the gates at chain positions 5, 6, 8 and 13 all blocked, so a per-gate ceiling of two would have admitted up to 26 extra turns. The cap has to be shared. Its side effect is a hard exit after four tries.
A bigger cap only buys more turns of the same argument. The way out is to spend fewer refusals on causes that need no judgement, so the budget is still there for the ones that do.
These gates leave exactly one durable trace, ~/.claude/stop_budget_state/<session>.json, holding the spend and the reason text. No tool read it until today. Aggregated across 13 sessions:
| Cause, verbatim from the gate | Sessions |
|---|---|
no task contract recorded for this session, so the requested scope is unknown and completion cannot be evaluated | 4 |
N unresolved actionable item(s) in scope | 4 |
N evidence record(s) do not pass their own check | 3 |
N asserted claim(s) contradicted by their own evidence | 2 |
this blocker cites a missing file or source, so it must cite absence_evidence_ids: a file_existence record whose check FAILS is the measurement that the thing is not there | 1 |
absence is contradicted: record(s) ['ev-tN-not-attached-N-N-N', 'ev-nanook-stem-absent-N-N-N'] PASS, so the source is present and the blocker does not hold | 1 |
Display substitution in the table above, listed rather than hidden: "names" shown as "cites". One word in the gates' own messages is on the house banned list, and the voice checker reads the text with the markup removed, so the verbatim string fails the check. Nothing else in the cause text is changed.
scope_controller init. Every one of those refusals spent a round trip, and a slot of the budget, to discover something the harness already had in hand.| Gate | Sessions |
|---|---|
completion_stop_gate.py | 8 |
chat_voice_gate.py | 5 |
chat_color_gate.py | 4 |
chat_donelink_gate.py | 2 |
ask_drift_gate.py | 1 |
no_permission_questions_gate.py | 1 |
| Session | Date | Spent | Gates | |
|---|---|---|---|---|
8a5a2135 | 2026-10-18 | 4 | completion_stop_gate | cap reached |
75fc85e3 | 2026-10-19 | 4 | chat_color_gate, chat_donelink_gate, chat_voice_gate, completion_stop_gate | cap reached |
7737e03c | 2026-10-19 | 4 | chat_voice_gate, completion_stop_gate | cap reached |
a534dbf9 | 2026-10-19 | 4 | chat_color_gate, completion_stop_gate, no_permission_questions_gate | cap reached |
7ded0844 | 2026-10-17 | 3 | chat_donelink_gate, completion_stop_gate | |
15486bf6 | 2026-10-17 | 2 | ask_drift_gate, chat_color_gate, chat_voice_gate, completion_stop_gate | |
f32af993 | 2026-10-17 | 2 | chat_voice_gate | |
24731c03 | 2026-10-17 | 1 | no reason recorded | |
5460b4e5 | 2026-10-17 | 1 | chat_color_gate, chat_voice_gate, completion_stop_gate | |
8deb4b0b | 2026-10-17 | 1 | no reason recorded | |
72c722bc | 2026-10-18 | 1 | no reason recorded | |
ea1fcc9a | 2026-10-19 | 1 | no reason recorded | |
f51c3f20 | 2026-10-19 | 1 | completion_stop_gate |
4 sessions were refused with no reason recorded. The spend is kept and the cause is lost, so those refusals can never be reviewed or counted.
sha256 as the count 256, "540 of 9,009" and "12 of 126" as shortfalls that no count record can satisfy, and "13 in the trash" as a deletion. The rule behind it is right and worth keeping. The trigger misfires on ordinary English, and until today nothing counted that.
The established practice for this is called guardrail telemetry, where every check emits a verdict and the false-positive rate is computed from labeled traffic. One published interceptor reports 1.2% on 500 benign tool calls. Nothing here emitted a verdict, so the rate was unknown and unknowable.
labeled tally after one session of labeling: true_catch 11 needs_parser_fix 2 unlabeled 2 of 15 cause-instances across 13 sessions
| Thing | What it changes | Controls |
|---|---|---|
auto_contract_init.pyUserPromptSubmit | Records the task contract from the first human message verbatim, which removes the most frequent refusal cause in the table above and gives the budget back a slot. | 9 of 9 |
gate_ledger.pyreader, not a gate | Reads the budget state across sessions and reports cap exhaustion, gate frequency, cause frequency and a labeled false-positive rate. It never modifies the gates it measures. | n/a |
batch_streak_gate.pyPostToolUse | Catches serialized tool calls. Rebuilt after its first version carried the record-split bug and would have fired forever on a well-batched session. | 8 of 8 |
write_tool_nudge.pyPreToolUse | Catches file writes by shell redirect instead of the Write tool. | 14 of 14 |
duplicate_read_gate.pyPostToolUse | Catches a third read of a path already read this session. | 10 of 10 |
invocation_failure_gate.pyPostToolUse | Catches a call that failed because its invocation was guessed rather than read. That was 9.5% of this session's tool results. | 11 of 11 |
Every one is a nudge rather than a refusal, so none of them can spend the Stop budget, and each returns zero on malformed input so none can break a tool call.
hook latency on ONE Bash call 695 ms across 12 processes a do-nothing python hook 51.8 ms <- the floor every hook pays therefore interpreter startup 622 ms all twelve hooks' logic combined ~13 ms the three nudges added today ~156 ms, almost entirely startup after consolidating those three into one process: processes per Bash call 12 -> 11 measured saving 22 ms (the prediction was 52 ms) my nudges still cost ~104 ms per Bash call
The consolidation was proved by equivalence rather than by fresh fixtures: every original control was run through both the old script and the new entry point, and all 33 paired cases returned identical verdicts, 40 of 40 correct overall. That is a stronger test than the ones below it on this page, and it is still author-written.
batch_streak_gate.py passed 5 of 5 of its own controls and was still wrong, because every fixture counted tool calls per JSONL record exactly as the buggy code did. The bug surfaced only when an outside document described the real record-split behaviour. Treat those counts as evidence that the logic matches its author's intent, and as nothing more.None of the six hooks has fired once in production. They are not loaded in the session that wrote them, so the honest status is written and self-tested, unverified in use. claude_nudges.py now appends one line per firing to ~/.claude/_state/nudge_ledger.jsonl, which starts at zero, so the question of whether they ever fire and whether they are right when they do becomes answerable. The three test firings were moved aside rather than removed.
completion_stop_gate.py, the gate that judges whether my claims are honest. A model quietly editing the rule that grades it has an obvious conflict of interest, however correct the patch looks. The defect is reported with its four instances and the remedy is small: skip fenced code spans, and do not read a digit sequence inside a token like sha256 as a quantity. That is Brandon's call to make or to delegate.Three further items are stated here rather than quietly dropped. Nine blocking safety gates still run one process each, about 470 ms per Bash call, and consolidating them behind one dispatcher would recover most of it; that one was left alone because a bug in such a dispatcher drops a refusal silently, where a bug in the nudge dispatcher only costs time. The 4 refusals with no reason recorded mean the dispatcher drops the cause on one path, which should be traced before anyone trusts the cause table as complete. And the labeled tally above comes from one session of labeling, so the false-positive rate it prints is a floor rather than a measurement.