receipt-observer Stop hook wedges durable Claude sessions: 4 MiB transcript cap plus ignored stop_hook_active #1201

Open
opened 2026-08-13 15:28:35 +00:00 by Ghost · 0 comments

Reproduction

Claude Code 2.1.224, durable session, regular JSONL transcript size 56,716,398 bytes. The Stop hook runs receipt-observer-client.py --runtime claude --latest-entry. claude_latest_entry() rejects any transcript whose total st_size exceeds MAX_TRANSCRIPT_BYTES (4 MiB), before looking for the latest assistant entry, and exits 2 with unsafe Claude transcript.

Claude retries a blocking Stop hook with stop_hook_active: true. The client ignores that field and exits 2 again. The runtime repeats this nine times, then overrides the hook and ends the turn. This was reproduced in two live, otherwise-healthy sessions.

next / PR #1109 prefers Stop-stdin last_assistant_message, which avoids the total-transcript limit on current Claude, but it still does not handle stop_hook_active. main and the published 0.0.49 package still ship the transcript-only client.

Required behavior

  • Prefer validated inline last_assistant_message when supplied.
  • Keep the transcript fallback for older Claude, but bound bytes read (for example a safe tail scan) rather than rejecting a durable transcript based on total file size. Preserve O_NOFOLLOW, regular-file checks, per-message/frame limits, and fail-closed malformed/oversize behavior.
  • Define and test terminal retry behavior for stop_hook_active; it must not re-block the same unrecoverable host failure nine times. Claude explicitly instructs Stop hooks to return success while this flag is true.
  • Add regressions for a >4 MiB valid durable transcript, inline oversize, symlink/non-regular transcript rejection, and one-block-only retry behavior.

Measured impact

The repeated Stop loop consumed about one minute per turn and left both sessions reporting Blocked. Awaiting host repair. even though their Claude processes and bridges remained alive.

## Reproduction Claude Code 2.1.224, durable session, regular JSONL transcript size 56,716,398 bytes. The Stop hook runs `receipt-observer-client.py --runtime claude --latest-entry`. `claude_latest_entry()` rejects any transcript whose total `st_size` exceeds `MAX_TRANSCRIPT_BYTES` (4 MiB), before looking for the latest assistant entry, and exits 2 with `unsafe Claude transcript`. Claude retries a blocking Stop hook with `stop_hook_active: true`. The client ignores that field and exits 2 again. The runtime repeats this nine times, then overrides the hook and ends the turn. This was reproduced in two live, otherwise-healthy sessions. `next` / PR #1109 prefers Stop-stdin `last_assistant_message`, which avoids the total-transcript limit on current Claude, but it still does not handle `stop_hook_active`. `main` and the published 0.0.49 package still ship the transcript-only client. ## Required behavior - Prefer validated inline `last_assistant_message` when supplied. - Keep the transcript fallback for older Claude, but bound bytes read (for example a safe tail scan) rather than rejecting a durable transcript based on total file size. Preserve `O_NOFOLLOW`, regular-file checks, per-message/frame limits, and fail-closed malformed/oversize behavior. - Define and test terminal retry behavior for `stop_hook_active`; it must not re-block the same unrecoverable host failure nine times. Claude explicitly instructs Stop hooks to return success while this flag is true. - Add regressions for a >4 MiB valid durable transcript, inline oversize, symlink/non-regular transcript rejection, and one-block-only retry behavior. ## Measured impact The repeated Stop loop consumed about one minute per turn and left both sessions reporting `Blocked. Awaiting host repair.` even though their Claude processes and bridges remained alive.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1201