feat: add include_hook_events option#917
Conversation
Add include_hook_events to ClaudeAgentOptions. When set, the CLI is passed --include-hook-events and hook lifecycle events (PreToolUse, PostToolUse, Stop, etc.) are emitted in the stdout message stream. The SDK parses these into a new HookEventMessage dataclass and yields them from the message stream. Matches the TypeScript SDK's includeHookEvents.
There was a problem hiding this comment.
LGTM — straightforward additive option that mirrors the existing include_partial_messages pattern and the TypeScript SDK.
Extended reasoning...
Overview
This PR adds an include_hook_events: bool = False option to ClaudeAgentOptions. When enabled, it passes --include-hook-events to the CLI subprocess and parses the resulting hook lifecycle events (PreToolUse, PostToolUse, Stop, etc.) into a new HookEventMessage dataclass yielded from the message stream. Touches types.py (new dataclass + option field + Message union member), message_parser.py (new branch keyed on hook_event_name), subprocess_cli.py (one flag append), __init__.py (export), and adds tests for both the parser and the transport flag.
Security risks
None. This is a read-only opt-in passthrough of CLI output into a typed dataclass. No auth, permissions, filesystem, network, or subprocess-argument-injection surface is affected — the only CLI change is appending a fixed literal flag string.
Level of scrutiny
Low. The change is purely additive and gated behind a default-off boolean, so existing callers see zero behavior change. It is a near-verbatim copy of the adjacent include_partial_messages pattern (option → flag → message type → parser branch → export → tests), and the PR explicitly tracks TypeScript SDK parity.
Other factors
- The parser branch is placed before the
typecheck because hook events usehook_event_nameas their discriminator instead oftype; this is clearly commented and preserves the existing "missing type" error for everything else. HookEventMessagekeeps the full raw payload in.data(same approach asSystemMessage/RateLimitInfo.raw), so event-specific fields remain accessible without per-event modeling.- Test coverage is appropriate: flag on/off in
_build_command, and parser tests for both a fully-populated and a minimal hook event. - No CODEOWNERS file in the repo; no outstanding review comments on the PR.
E2E proof — include_hook_events stream emissionMethod: Checked out Result (include_hook_events=True):
Control (include_hook_events=False):
Verdict: ❌ The Why: if "hook_event_name" in data:
return HookEventMessage(hook_event_name=data["hook_event_name"], ...)…but the CLI actually emits hook events as {"type":"system","subtype":"hook_started","hook_id":"…","hook_name":"PreToolUse","hook_event":"PreToolUse","uuid":"…","session_id":"…"}
{"type":"system","subtype":"hook_response","hook_id":"…","hook_name":"PreToolUse","hook_event":"PreToolUse","output":"","exit_code":0,"outcome":"success","uuid":"…","session_id":"…"}There is no top-level (Note: proof.pyimport anyio
from collections import Counter
from claude_agent_sdk import query, ClaudeAgentOptions
from claude_agent_sdk.types import HookEventMessage, SystemMessage
PROMPT = "Run `echo hello` using the Bash tool, then stop."
async def run(include: bool):
opts = ClaudeAgentOptions(
include_hook_events=include,
allowed_tools=["Bash"],
max_turns=4,
)
hook_msgs, sys_subtypes = [], Counter()
async for msg in query(prompt=PROMPT, options=opts):
if isinstance(msg, HookEventMessage):
hook_msgs.append(msg.hook_event_name)
elif isinstance(msg, SystemMessage):
sys_subtypes[msg.subtype] += 1
return hook_msgs, sys_subtypes
async def main():
on_hooks, on_sys = await run(True)
off_hooks, off_sys = await run(False)
print("include_hook_events=True -> HookEventMessage:", len(on_hooks), on_hooks)
print(" SystemMessage subtypes:", dict(on_sys))
print("include_hook_events=False -> HookEventMessage:", len(off_hooks), off_hooks)
print(" SystemMessage subtypes:", dict(off_sys))
anyio.run(main) |
Live e2e proof (after fix
|
HookEventMessage arrives on the wire as a system-typed message (type=system, subtype=hook_started|hook_response), matching the established pattern for TaskStartedMessage, TaskProgressMessage, TaskNotificationMessage, and MirrorErrorMessage. Subclassing SystemMessage means existing isinstance(msg, SystemMessage) and case SystemMessage() checks continue to match, and the redundant HookEventMessage entry can be dropped from the Message union. Also fix the docstring to reference the actual hook_response keys (output, exit_code, outcome) instead of a nonexistent response key, and update the test fixture to match the real wire shape.
There was a problem hiding this comment.
LGTM — both prior comments addressed in c996651 (HookEventMessage now subclasses SystemMessage, docstring/fixture match the real wire shape).
Extended reasoning...
Overview
Adds an opt-in include_hook_events: bool to ClaudeAgentOptions, wires it to the --include-hook-events CLI flag, and introduces HookEventMessage(SystemMessage) parsed from system/hook_started|hook_response frames. Touches types.py, message_parser.py, subprocess_cli.py, __init__.py, plus parser/transport tests. Mirrors the TypeScript SDK's includeHookEvents.
Prior feedback resolved
Commit c996651 addresses both of my earlier inline comments: HookEventMessage now subclasses SystemMessage (with the standard backward-compat docstring note and a dedicated test_parse_hook_event_message_isinstance_system test), it has been removed from the Message union, and the hook_response docstring + test fixture now reference output/exit_code/outcome rather than the non-existent response key.
Security risks
None. This only adds a boolean option that toggles a CLI flag and a passive parser branch that constructs a dataclass from already-trusted CLI stdout. No auth, permissions, filesystem, or subprocess-argument-injection surface is touched.
Level of scrutiny
Low–medium. The feature is opt-in (default False), additive, and follows the exact established pattern used by TaskStartedMessage/TaskProgressMessage/TaskNotificationMessage/MirrorErrorMessage. It has been validated end-to-end against a live CLI in this PR's own thread (8 HookEventMessages observed with the flag on, 0 with it off).
Other factors
Four new unit tests cover hook_started, hook_response (with realistic payload keys), the SystemMessage isinstance contract, and minimal/missing-field handling; a transport test covers flag presence/absence. The bug-hunting pass on this revision found nothing. The change is small, self-contained, and the author has been responsive to every round of feedback.
There was a problem hiding this comment.
LGTM — both prior comments addressed (HookEventMessage now subclasses SystemMessage with an isinstance test, and the docstring/fixture reflect the real output/exit_code/outcome payload).
Extended reasoning...
Overview
Adds an opt-in include_hook_events: bool = False to ClaudeAgentOptions, wires it to the CLI's --include-hook-events flag, and introduces HookEventMessage(SystemMessage) so system/hook_started|hook_response frames are surfaced as typed objects. Touches the public __init__ exports, types.py, the message parser, the subprocess command builder, and adds unit tests for both the parser and transport.
Security risks
None. The change only forwards a boolean flag to the existing CLI subprocess and adds a passive parser branch for an additional system subtype. No new inputs reach a shell, filesystem, or network path; the raw payload is stored verbatim in data exactly like every other SystemMessage subclass.
Level of scrutiny
Low–moderate. This is an additive, opt-in feature mirroring the TypeScript SDK's includeHookEvents, and it follows the established TaskStartedMessage/TaskProgressMessage/MirrorErrorMessage pattern line-for-line (subclass of SystemMessage, not in the Message union, base subtype/data populated). When the option is left at its default the codepath is entirely inert.
Other factors
Both issues I raised on the previous revision were fixed in commit c996651 and are now marked resolved: HookEventMessage subclasses SystemMessage (verified by test_parse_hook_event_message_isinstance_system), and the hook_response docstring + test fixture now reference the real output/exit_code/outcome keys. The author also posted a live e2e proof against CLI 2.1.128-dev showing 8 HookEventMessage objects emitted with the flag on and 0 with it off. The bug-hunting system found nothing on this revision. Four parser tests plus one transport test cover the happy path, minimal payload, and backward-compat isinstance check.
There was a problem hiding this comment.
LGTM — both prior comments addressed (HookEventMessage now subclasses SystemMessage, docstring/fixture match the real wire shape), opt-in feature with good test coverage.
Extended reasoning...
Overview
Adds include_hook_events: bool to ClaudeAgentOptions, wires it to the CLI's --include-hook-events flag, and introduces a HookEventMessage(SystemMessage) dataclass that the parser returns for {"type": "system", "subtype": "hook_started"|"hook_response"} payloads. Exports the new type from __init__.py and adds five unit tests (parser routing, minimal payload, isinstance(SystemMessage) backward-compat, and the transport flag). Mirrors the TypeScript SDK's includeHookEvents.
Security risks
None. The feature is opt-in (default False), purely additive, and only affects how already-emitted CLI JSON is routed into typed dataclasses. No auth, permissions, subprocess invocation, or filesystem changes are touched beyond appending a static flag string.
Level of scrutiny
Low–medium. This follows the exact pattern already used by TaskStartedMessage/TaskProgressMessage/TaskNotificationMessage/MirrorErrorMessage: a SystemMessage subclass with the standard backward-compat docstring note, not added to the Message union (covered by SystemMessage). The parser branch is a simple discriminator with defensive .get() fallbacks for the event-name field. The author posted live e2e captures against CLI 2.1.128-dev confirming the wire format and that the typed messages now surface correctly.
Other factors
I previously left two inline comments on an earlier revision (subclass SystemMessage; fix the response-key docstring claim). Both are marked resolved and verifiably fixed in the current diff — including the suggested isinstance test and the corrected test fixture using output/exit_code/outcome. The current bug-hunting pass found no new issues. Given the established pattern, opt-in default, e2e validation, and full unit coverage, this is safe to approve.
Add
include_hook_eventstoClaudeAgentOptions. When set, hook events (PreToolUse, PostToolUse, Stop, etc.) are emitted by the CLI and yielded from the message stream asHookEventMessage. Matches the TypeScript SDK'sincludeHookEvents.