Stop hook block reason is sent to the model twice (meta message + hook_blocking_error attachment)
Status Open
Reported on v2.1.280
Maintainer reply None cached
Activity 0 comments · opened Sep 25, 2026
What happens
When a command-type Stop hook blocks (exit 2 with a reason on stderr, or {"decision":"block","reason":…}), the reason reaches the model twice in the next request:
- as a meta user message:
Stop hook feedback:\n<reason>(isMeta: true), and - as a
hook_blocking_errorattachment carrying the same text inblockingError.blockingError.
Both records are written to the session transcript side by side, and both are rendered into the request, so every block reason costs twice its tokens, and the duplicate is then re-read from cache on every later turn.
Evidence
- Claude Code 2.1.280, macOS, interactive and
-p. - Across 14 days of our transcripts, 2,588 of 2,588 command-hook Stop blocks carried both copies.
- The prompt grows between the blocked turn and the next request by the model's output plus both copies: the implied chars/token matches the ~2.5 we measure for this prose only when the attachment is counted too.
- For a hook-heavy setup the second copy is a few tenths of a percent of total token spend: small, but pure duplication.
Repro
- Add a Stop hook:
{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"echo 'please run the tests first' >&2; exit 2"}]}]}}(make it block only once, e.g. by checking a marker file). - Run any prompt so the session stops and the hook blocks.
- In the session's
.jsonl, the blocked turn has both a{"type":"user","isMeta":true,"message":{"content":"Stop hook feedback:\nplease run the tests first"}}record and a{"type":"attachment","attachment":{"type":"hook_blocking_error","hookEvent":"Stop","blockingError":{"blockingError":"please run the tests first",…}}}record, and the request that follows contains the reason text twice.
Expected
The reason is sent once: either the meta message or the attachment, not both.