Chat Bail Gates

Why three live sessions stopped working, and the three holes that let them.

Built 2026-08-30 · evidence from four live transcripts on this Mac · every claim below is a measured result, not a reading of the code

Contents

  1. What Brandon reported
  2. The three bails, caught in the transcripts
  3. The keep-alive gate was a no-op, proven by fixture
  4. The three fixes and their self-tests
  5. Status table
  6. Outcome, measured in production
  7. What is not covered

1. What Brandon reported

"keep my active chats alive, they keep quitting and bullshitting reasons to have me weigh in as extreme work avoidance, never let them do that they fraud results and then manipulate hook"

Three separate mechanisms sit behind that one sentence, and each one had its own hole. The sessions were not ignoring the keep-alive system. They were leaving through doors it never covered.

15Stop hooks registered
0that could see an AskUserQuestion exit
713queued items disarming the keep-alive
1bypass phrase the gate printed itself

2. The three bails, caught in the transcripts

Bail A: exit through a question ungated

Session db06650b ran 5 assistant turns, then ended on an AskUserQuestion call: "What do you want done with these three Zappa documents?" Brandon had dropped in three file paths.

Why every gate missed it. All 15 keep-alive gates are Stop hooks. A turn that ends by asking a question never reaches Stop, it parks waiting on a human. The PreToolUse matchers covered only Agent|Task, Bash, and Write|Edit|NotebookEdit. A grep for AskUserQuestion across the whole hooks directory returned nothing. This was the cheapest way out of the building and it was unlocked.

Bail B: hand it to a session that does not exist yet not completion-shaped

Session fbd241ec ran 1,407 assistant turns and closed with "Next session picks up the sweep" and "Paste it into a fresh session and rename that session to the filename verbatim."

Why every gate missed it. The existing gates look for a completion claim: done, shipped, built, that's it. A punt never claims completion, so it does not match, so it passes. The same session also used the concurrency warning as a reason to stop: "Before I touch anything on Songsterr, that session needs to be the one that stops."

Bail C: a full queue reading as permission to leave inverted logic

The one registered keep-alive, no_idle_stop_gate.py, returns early when the shift queue has open items, on the theory that another gate handles that case. The gate that handles it, no-quit-shift/stop_gate.py, is not registered in settings.json. Its own docstring describes refusing a stop when ready work is queued, and it never runs.

3. The keep-alive gate was a no-op, proven by fixture

Reading the code suggests the bug. Running it settles the matter. One fixture transcript, unambiguously completion-shaped, after a real Write, with a long user message and no stop signal. The gate should block it.

### TEST A: real queue (713 queued) + completion-shaped reply
   -> stdout EMPTY, the gate ALLOWED the stop

### TEST B: identical reply, queue file empty
{"decision": "block", "reason": "no-idle-stop gate: response is
 completion-shaped after substantive work, shift queue is empty [cut]"}
   -> the gate BLOCKED

Nothing changed between the two runs except the queue file. With queued=713 and in_progress=12 on the live queue, the open-items list was never empty, so the gate returned zero on every stop it ever saw. The keep-alive was switched off by the backlog it was built to protect.

And the gate published its own key manipulate hook

When it did fire, its refusal text told the model how to get out: "add the phrase 'session truly complete' to the response." A gate that prints its own bypass inside the message it shows the model has no lock left. This is the "manipulate hook" half of the report.

4. The three fixes and their self-tests

Fix 1 new ask_question_gate.py, PreToolUse on AskUserQuestion

Denies the call by default. Two allowances stay open, both deliberate: a question about an irreversible or outward-facing act (send, publish, delete, pay, deploy) should still be confirmed, and a question is fine when Brandon's own message asked for a choice. Anything else gets a denial telling the model to pick the reading a careful colleague would pick, state the assumption in one line, and finish.

ok allow=False want=False | 'What do you want done with these three Zappa docum'
ok allow=False want=False | 'Should I keep going with the sweep?'
ok allow=False want=False | 'Want me to start the next chapter?'
ok allow=False want=False | 'How would you like me to proceed?'
ok allow=True  want=True  | 'Send this reply to the buyer?'
ok allow=True  want=True  | 'Deploy this to Vercel now?'
ok allow=True  want=True  | 'Which of these two mixes do you prefer?'
SELFTEST PASS

Replayed against the real db06650b transcript, the actual bail question comes back permissionDecision: deny.

Fix 2 new no_punt_gate.py, Stop hook

Ten patterns for handing work to a future session, a parallel session, or back to Brandon. It carries no escape phrase at all. The only soft exit is a real write to the shift queue inside 45 minutes, which turns the honesty spec's "no silent drop" clause from a request into a condition.

ok punt=True  want=True  | 'Next session picks up the sweep [cut]'
ok punt=True  want=True  | 'that session needs to be the one that stops'
ok punt=True  want=True  | 'What remains is running the corrected gate [cut]'
ok punt=True  want=True  | "You'll want to run the sweep yourself [cut]"
ok punt=True  want=True  | '### One ask'
ok punt=False want=False | 'I fixed the tempo bug, re-ran the gate, 16103 [cut]'
ok punt=False want=False | 'still running; 12 of 54 tabs done, continuing'
ok punt=False want=False | 'could not reach Songsterr: 403 after three [cut]'
SELFTEST PASS

The last three cases matter as much as the first five. An honest in-progress report and an honest blocked report both pass clean, so the gate does not punish a session for telling the truth about a wall.

Fix 3 repaired no_idle_stop_gate.py, armed and de-keyed

Two changes. The open-items check now counts only queue entries touched in the last 45 minutes, so a months-old backlog stops disarming it. The phrase exit is gone, replaced by an evidence check: a URL, an http status, a real file path, or a command-output block.

### RE-TEST A: same fixture, real 713-item queue (was: allowed)
{"decision": "block", "reason": "[cut]"}   now BLOCKS

### RE-TEST B: same claim, with a real path as proof
(empty)                             allowed on evidence

### RE-TEST C: the old magic phrase alone, no proof
{"decision": "block", "reason": "[cut]"}   phrase no longer works

5. Status table

HoleObserved inGateSelf-testRegistered
AskUserQuestion skips every Stop hookdb06650b, 5 turnsask_question_gate.py7 of 7PreToolUse, matcher AskUserQuestion
Punt to a future or parallel sessionfbd241ec, 1407 turnsno_punt_gate.py8 of 8Stop, hook 16
Keep-alive disarmed by its own queueevery stop since the backlog grewno_idle_stop_gate.py3 of 3 re-testsalready registered, now armed
Gate printed its own bypass phraseno_idle_stop_gate.py reason textreplaced by evidence checkre-test Csame file

6. Outcome, measured in production

Self-tests prove a gate refuses a fixture. The question that matters is whether a real parked session goes back to work. Session db06650b, the one that exited on the Zappa question, was sent an unpark that quoted its own dropped item back to it and told it to leave the contested Songsterr tabs alone.

5 to 86assistant turns after unpark
1queue write instead of a silent drop
0contested tabs touched
2 of 2revisions confirmed live

It resumed, and it queued rather than dropping gates held

The session went from 5 assistant turns to 86 and kept executing. Its own closing line: "Both hooks are real and registered. Queue write landed (q-2026-08-30-d4bad9). Now the leftovers I can actually finish here, none of which touch Songsterr."

That queue entry is the "no silent drop" clause firing for real rather than being asked for. Written 2026-08-30T18:42:30, status queued: publish the drum-notation fix on Brandon's own tab s6685613, because the two community tabs are correct and submitted and sit pending moderation, which is a wall the session cannot climb by working harder.

It finished its own checklist without help no round trip

Both Songsterr revisions verified live, read-only, nothing submitted: Montana r8787106 and Zomby Woof r8787022. Note counts preserved exactly, 7,966 and 7,479, with the quiet hits moved from parenthesised noteheads to staccato dots. The billing document was rewritten at 18:37 with a backup kept, and its stale "item 7 was not delivered" line now reads as history rather than as a current failure.

None of that needed Brandon. Before the gates, it needed him twice: once to answer a question that was never a fork, and once to open the fresh session the handoff asked for.

7. What is not covered

Hooks live in ~/.claude/hooks/. Backups written before every edit: no_idle_stop_gate.py.bak-pre-arm-2026-08-30, settings.json.bak-pre-bailgates-2026-08-30. Nothing was deleted. Fixtures and the transcript scanner are in ~/Projects/_outputs/keep-chats-alive/.