Hooks docs: four payload field divergences vs shipped behavior (2.1.207, captured)

Status Fixed / completed
Maintainer reply ✓ Yes — bcherny
Activity 5 comments · opened Jul 13, 2026 · closed Aug 16, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

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.

View original on GitHub ↗

4 Comments

bcherny collaborator · 1 month ago

Thanks for the careful capture. I re-ran your tee probe on the released 2.1.233 (macOS, claude -p with hooks in .claude/settings.local.json) and got the same shipped payloads you did: PostToolUse carries tool_response (object), SessionEnd carries reason, UserPromptSubmit carries prompt, and SessionStart has no permission_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 documents reason (with the reason table), the UserPromptSubmit example uses prompt, and the SessionStart example JSON omits permission_mode; the common-fields table says "Not all events receive this field. Check the JSON example in each hook event section." I couldn't find tool_output, end_reason, or user_prompt anywhere 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_prompt on 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

cameronsjo · 1 month ago

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.txt and 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. A git log -S over every committed revision of that file returns zero commits for tool_output, zero for end_reason, and zero for user_prompt. Widening to every archived docs page, only user_prompt appears 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_response in the PostToolUse example, reason documented for SessionEnd with its table, prompt in the UserPromptSubmit example. I re-fetched the live page today and grepped it too — zero for all three, against non-zero controls for tool_response and permission_mode in 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_mode is 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.

cameronsjo · 1 month ago

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/hooks and 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_input implies tool_output, SessionEnd implies end_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.

bcherny collaborator · 1 month ago

Thanks for the captured payloads. All four points are now reflected in the hooks reference: PostToolUse uses tool_response, SessionEnd uses reason, UserPromptSubmit uses prompt, and the SessionStart example payload no longer includes permission_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-events

Closing as resolved.

🤖 Generated with Claude Code

Showing cached comments. Read the full discussion on GitHub ↗