Hooks docs: four payload field divergences vs shipped behavior (2.1.207, captured)
Summary
Four divergences between the hooks reference documentation and the payloads Claude Code actually delivers, all captured live on v2.1.207 (macOS) with a minimal probe: a project whose .claude/settings.local.json defines hooks that tee stdin to a file, driven by headless claude -p sessions.
Observed vs documented
| Event | Documented | Observed (captured payload) |
|---|---|---|
| PostToolUse | tool_output | tool_response (object) |
| SessionEnd | end_reason | reason |
| UserPromptSubmit | user_prompt | prompt |
| SessionStart | (common-fields table can read as if permission_mode is universal) | no permission_mode on SessionStart; present on UserPromptSubmit / PreToolUse / Stop |
On the fourth row: the current docs do say "Not all events receive this field," which is accurate — the ask is just to make each event's example JSON authoritative and complete, since a hook gating on a field its event never receives fails silently (empty var → the false branch, forever, with no error surface).
Suggested fix
Align the per-event example payloads in the hooks reference with the shipped field names above, and consider noting per event whether permission_mode is delivered.
Raw captured payloads available on request — they're single-line JSON files produced by the tee probe described above, trivially reproducible on any machine.
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
Thanks for the careful capture. I re-ran your
teeprobe on the released 2.1.233 (macOS,claude -pwith hooks in.claude/settings.local.json) and got the same shipped payloads you did: PostToolUse carriestool_response(object), SessionEnd carriesreason, UserPromptSubmit carriesprompt, and SessionStart has nopermission_mode(it is present on UserPromptSubmit / PreToolUse / PostToolUse / Stop).Those shipped names are what the current hooks reference documents, though. On https://code.claude.com/docs/en/hooks the PostToolUse example uses
tool_response, the SessionEnd input section documentsreason(with the reason table), the UserPromptSubmit example usesprompt, and the SessionStart example JSON omitspermission_mode; the common-fields table says "Not all events receive this field. Check the JSON example in each hook event section." I couldn't findtool_output,end_reason, oruser_promptanywhere on that page, or in its history back to at least April 2026, so those names may have come from an older or third-party copy of the docs.If you're still seeing
tool_output/end_reason/user_prompton the official reference, could you paste the exact URL and section? Otherwise I think the docs already match shipped behavior and this can be closed.🤖 Generated with Claude Code
You're right, and there's no URL to give you — rows 1-3 were wrong.
I checked it against a record rather than my memory. I keep a local archive that polls every page in
code.claude.com/docs/llms.txtand commits each one to git, so the hooks reference has a per-revision history running 2026-03-12 to 2026-08-12 — your April window and four months before it. Agit log -Sover every committed revision of that file returns zero commits fortool_output, zero forend_reason, and zero foruser_prompt. Widening to every archived docs page, onlyuser_promptappears at all, and every hit is the OpenTelemetry event name in the monitoring and SDK pages, never a hook field. The snapshot from the filing date says what you say:tool_responsein the PostToolUse example,reasondocumented for SessionEnd with its table,promptin the UserPromptSubmit example. I re-fetched the live page today and grepped it too — zero for all three, against non-zero controls fortool_responseandpermission_modein the same pass.The failure was a process one on my end. The "Observed" column came from captured payloads; the "Documented" column was written from recollection and never re-read against the live page before filing. Sorry for the round trip.
Row 4 was never a divergence claim — the issue already granted the caveat is accurate, and the archive shows "Not all events receive this field" landing 2026-03-22, four months before I filed. What's left is only a preference: that each event's example JSON be explicit about whether
permission_modeis delivered, since a hook gating on a field its event never receives takes the false branch forever with no error surface. Small enough that closing this outright is fine by me. Your call.Traced where the three names actually came from, since "an older or third-party copy of the docs" turned out not to be it.
They were fabricated by a summarizing page-fetch — the kind of tool that retrieves a URL and answers a question about it with a small model, rather than returning the text. On the filing date it was pointed at
code.claude.com/docs/en/hooksand asked to list every event's input fields. It returned a clean, confident per-event breakdown containing "tool_output: tool's result" under PostToolUse, "Common fields +end_reason" under SessionEnd, and "user_prompt: the submitted prompt text" under UserPromptSubmit. None of those are on the page. The captured payloads were then diffed against that summary rather than against the page, and the divergences fell out mechanically.Two things make it worth mentioning here. The invented names are all regularizations of real neighbors —
tool_inputimpliestool_output,SessionEndimpliesend_reason— so they read as more plausible than the truth. And the failure is invisible from the inside: a fetch that summarizes feels like having read the page, so nothing prompts a re-check. Reproduced it again this week on the same URL, where the same tool asserted all three strings were present and attributed them to a truncated section; a plain fetch-and-grep with controls returned zero.So: a docs-divergence report needs the documented side established by retrieving and searching the text, never by asking something to summarize it. Filing that under lessons rather than as a request — nothing here changes the disposition of this issue.
Thanks for the captured payloads. All four points are now reflected in the hooks reference: PostToolUse uses
tool_response, SessionEnd usesreason, UserPromptSubmit usesprompt, and the SessionStart example payload no longer includespermission_mode(the common-fields table notes that not every event receives it and points to each event's example JSON as authoritative). See https://code.claude.com/docs/en/hooks#hook-eventsClosing as resolved.
🤖 Generated with Claude Code