Add SubagentStop hook (#22873)

# What

<img width="1792" height="1024" alt="image"
src="https://github.com/user-attachments/assets/8f81d232-5813-4994-a61d-e42a05a93a3e"
/>

`SubagentStop` runs when a thread-spawned subagent turn is about to
finish. Thread-spawned subagents use `SubagentStop` instead of the
normal root-agent `Stop` hook.

Configured handlers match on `agent_type`. Hook input includes the
normal stop fields plus:

- `agent_id`: the child thread id.
- `agent_type`: the resolved subagent type.
- `agent_transcript_path`: the child subagent transcript path.
- `transcript_path`: the parent thread transcript path.
- `last_assistant_message`: the final assistant message from the child
turn, when available.
- `stop_hook_active`: `true` when the child is already continuing
because an earlier stop-like hook blocked completion.

`SubagentStop` shares the same completion-control semantics as `Stop`,
scoped to the child turn:

- No decision allows the child turn to finish.
- `decision: "block"` with a non-empty `reason` records that reason as
hook feedback and continues the child with that prompt.
- `continue: false` stops the child turn. If `stopReason` is present,
Codex surfaces it as the stop reason.

# Lifecycle Scope

Only thread-spawned subagents run `SubagentStop`.

Internal/system subagents such as Review, Compact, MemoryConsolidation,
and Other do not run normal `Stop` hooks and do not run `SubagentStop`.
This avoids exposing synthetic matcher labels for internal
implementation paths.

# Stack

1. #22782: add `SubagentStart`.
2. This PR: add `SubagentStop`.
3. #22882: add subagent identity to normal hook inputs.
This commit is contained in:
Abhinav
2026-05-20 14:59:41 -07:00
committed by GitHub
Unverified
parent 40ad7be2b5
commit eee3e60db3
43 changed files with 813 additions and 77 deletions
+8 -1
View File
@@ -47,6 +47,8 @@ pub struct HookEventsToml {
pub user_prompt_submit: Vec<MatcherGroup>,
#[serde(rename = "SubagentStart", default)]
pub subagent_start: Vec<MatcherGroup>,
#[serde(rename = "SubagentStop", default)]
pub subagent_stop: Vec<MatcherGroup>,
#[serde(rename = "Stop", default)]
pub stop: Vec<MatcherGroup>,
}
@@ -62,6 +64,7 @@ impl HookEventsToml {
session_start,
user_prompt_submit,
subagent_start,
subagent_stop,
stop,
} = self;
pre_tool_use.is_empty()
@@ -72,6 +75,7 @@ impl HookEventsToml {
&& session_start.is_empty()
&& user_prompt_submit.is_empty()
&& subagent_start.is_empty()
&& subagent_stop.is_empty()
&& stop.is_empty()
}
@@ -85,6 +89,7 @@ impl HookEventsToml {
session_start,
user_prompt_submit,
subagent_start,
subagent_stop,
stop,
} = self;
[
@@ -96,6 +101,7 @@ impl HookEventsToml {
session_start,
user_prompt_submit,
subagent_start,
subagent_stop,
stop,
]
.into_iter()
@@ -104,7 +110,7 @@ impl HookEventsToml {
.sum()
}
pub fn into_matcher_groups(self) -> [(HookEventName, Vec<MatcherGroup>); 9] {
pub fn into_matcher_groups(self) -> [(HookEventName, Vec<MatcherGroup>); 10] {
[
(HookEventName::PreToolUse, self.pre_tool_use),
(HookEventName::PermissionRequest, self.permission_request),
@@ -114,6 +120,7 @@ impl HookEventsToml {
(HookEventName::SessionStart, self.session_start),
(HookEventName::UserPromptSubmit, self.user_prompt_submit),
(HookEventName::SubagentStart, self.subagent_start),
(HookEventName::SubagentStop, self.subagent_stop),
(HookEventName::Stop, self.stop),
]
}