Stop hook fires before the final assistant message is flushed to the transcript; hooks reading the transcript get the previous turn's message (breaks read-aloud accessibility)
Summary
The Stop hook fires before the just-finished assistant message is flushed to the transcript file. A Stop hook that reads the final message out of transcript_path therefore **intermittently gets the previous turn's message (a one-turn lag), or nothing early in a session. Reading the message from the Stop input field last_assistant_message** instead is race-free and fixes it — but that field does not appear to be documented, so the natural (and broken) implementation is to parse the transcript.
Why this matters (accessibility)
I'm a blind developer. Because the Claude Code desktop app does not expose assistant replies to VoiceOver, I rely on a Stop hook that speaks each final reply aloud with macOS say. When the hook reads the previous turn's text, I hear a stale answer to a question I already moved past — confusing and, for a screen-reader user, genuinely disorienting since audio is my only channel. So a transcript read that is "usually fine" is not good enough here.
Repro
- Register a
Stophook whose script reads the last assistant text block fromtranscript_path(e.g.jq -rs '[.[]|select(.type=="assistant")|.message.content[]?|select(.type=="text")|.text]|last' "$transcript") and speaks/prints it. - Have a normal multi-turn conversation.
- Intermittently, the hook emits the previous turn's text rather than the current one. Frequency varies with timing/turn length (it's a race), which is what made it so hard to pin down.
Evidence it's a write-vs-read race
I logged every firing (timestamp + the length of the text the hook extracted). On a lagged turn, the hook read text of exactly the prior turn's length. Dumping the transcript a couple of seconds later showed the current turn's assistant block was present and last — i.e. it was written after the hook had already fired and read. So transcript_path is not guaranteed to contain the final message at the moment Stop fires.
The fix that worked
The raw Stop hook stdin already carries the finished message directly. Observed fields in this build:
session_id, transcript_path, cwd, prompt_id, permission_mode, effort,
hook_event_name, stop_hook_active, last_assistant_message,
background_tasks, session_crons
Switching the hook to read .last_assistant_message (falling back to the transcript only if absent) eliminated the lag completely.
Requests
- Document
last_assistant_message(and ideally the fullStophook input schema) in the hooks docs, and note that reading the just-finished message out of the transcript atStoptime is racy — steer hook authors to the input field. - Or/and guarantee the transcript is flushed with the final assistant message before
Stophooks fire, so transcript-reading hooks are reliable. - Consider calling this out specifically for accessibility / read-aloud hook use cases, which are the ones this silently breaks.
Environment
- Claude Code, macOS desktop app (Darwin 25.5.0).
- Reproduced and worked around 2026-07-04.