Skip to content

Commit 6164908

Browse files
committed
fix: correctly parse hook events from system/hook_started|hook_response messages
1 parent e37e059 commit 6164908

3 files changed

Lines changed: 51 additions & 8 deletions

File tree

src/claude_agent_sdk/_internal/message_parser.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,23 @@ def parse_message(data: dict[str, Any]) -> Message | None:
4949
data,
5050
)
5151

52-
# Hook events (emitted when ``include_hook_events`` is enabled) carry a
53-
# ``hook_event_name`` discriminator rather than a standard ``type`` field.
54-
if "hook_event_name" in data:
52+
# Hook events (emitted when ``include_hook_events`` is enabled) arrive as
53+
# ``system`` messages with ``subtype`` of ``hook_started`` or
54+
# ``hook_response``. Route them to ``HookEventMessage`` before the generic
55+
# ``SystemMessage`` handling below.
56+
if data.get("type") == "system" and data.get("subtype") in (
57+
"hook_started",
58+
"hook_response",
59+
):
60+
hook_event_name = (
61+
data.get("hook_event")
62+
or data.get("hook_name")
63+
or data.get("hook_event_name")
64+
or ""
65+
)
5566
return HookEventMessage(
56-
hook_event_name=data["hook_event_name"],
67+
subtype=data["subtype"],
68+
hook_event_name=hook_event_name,
5769
data=data,
5870
session_id=data.get("session_id"),
5971
uuid=data.get("uuid"),

src/claude_agent_sdk/types.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,7 +1161,13 @@ class HookEventMessage:
11611161
message stream. Each event is identified by ``hook_event_name`` and the
11621162
full raw payload is available in ``data``.
11631163
1164+
These arrive on the wire as ``{"type": "system", "subtype":
1165+
"hook_started" | "hook_response", "hook_event": "PreToolUse", ...}``.
1166+
11641167
Attributes:
1168+
subtype: Lifecycle phase — ``"hook_started"`` when a hook begins
1169+
executing, ``"hook_response"`` when it completes (the latter
1170+
carries a ``response`` key in ``data``).
11651171
hook_event_name: Name of the hook event (e.g. ``"PreToolUse"``,
11661172
``"PostToolUse"``, ``"Stop"``).
11671173
data: Full raw event dict from the CLI, including any
@@ -1170,6 +1176,7 @@ class HookEventMessage:
11701176
uuid: Unique ID of the event, if present.
11711177
"""
11721178

1179+
subtype: str
11731180
hook_event_name: str
11741181
data: dict[str, Any] = field(default_factory=dict)
11751182
session_id: str | None = None

tests/test_message_parser.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -967,26 +967,50 @@ def test_parse_result_message_success_no_errors(self):
967967
assert message.result == "Task completed successfully"
968968

969969
def test_parse_hook_event_message(self):
970-
"""Test parsing a hook event into HookEventMessage."""
970+
"""Hook started events (system/hook_started) parse into HookEventMessage."""
971971
data = {
972-
"hook_event_name": "PreToolUse",
972+
"type": "system",
973+
"subtype": "hook_started",
974+
"hook_event": "PreToolUse",
975+
"hook_name": "PreToolUse",
973976
"session_id": "sess-123",
974977
"uuid": "uuid-456",
975978
"tool_name": "Bash",
976979
"tool_input": {"command": "ls"},
977980
}
978981
message = parse_message(data)
979982
assert isinstance(message, HookEventMessage)
983+
assert message.subtype == "hook_started"
980984
assert message.hook_event_name == "PreToolUse"
981985
assert message.session_id == "sess-123"
982986
assert message.uuid == "uuid-456"
983987
assert message.data == data
984988

989+
def test_parse_hook_event_message_response(self):
990+
"""Hook response events (system/hook_response) parse into HookEventMessage."""
991+
data = {
992+
"type": "system",
993+
"subtype": "hook_response",
994+
"hook_event": "PostToolUse",
995+
"hook_name": "PostToolUse",
996+
"session_id": "sess-123",
997+
"uuid": "uuid-789",
998+
"response": {"decision": "approve"},
999+
}
1000+
message = parse_message(data)
1001+
assert isinstance(message, HookEventMessage)
1002+
assert message.subtype == "hook_response"
1003+
assert message.hook_event_name == "PostToolUse"
1004+
assert message.session_id == "sess-123"
1005+
assert message.uuid == "uuid-789"
1006+
assert message.data["response"] == {"decision": "approve"}
1007+
9851008
def test_parse_hook_event_message_minimal(self):
986-
"""Hook events without session_id/uuid still parse."""
987-
data = {"hook_event_name": "Stop"}
1009+
"""Hook events without session_id/uuid/hook_event still parse."""
1010+
data = {"type": "system", "subtype": "hook_started", "hook_name": "Stop"}
9881011
message = parse_message(data)
9891012
assert isinstance(message, HookEventMessage)
1013+
assert message.subtype == "hook_started"
9901014
assert message.hook_event_name == "Stop"
9911015
assert message.session_id is None
9921016
assert message.uuid is None

0 commit comments

Comments
 (0)