Add SubagentStart hook (#22782)

# What

`SubagentStart` runs once when Codex creates a thread-spawned subagent,
before that child sends its first model request. Thread-spawned
subagents use `SubagentStart` instead of the normal root-agent
`SessionStart` hook.

Configured handlers match on the subagent `agent_type`, using the same
value passed to `spawn_agent`. When no agent type is specified, Codex
uses the default agent type.

Hook input includes the normal session-start fields plus:

- `agent_id`: the child thread id.
- `agent_type`: the resolved subagent type.

`SubagentStart` may return `hookSpecificOutput.additionalContext`. That
context is added to the child conversation before the first model
request.

# Lifecycle Scope

Only thread-spawned subagents run `SubagentStart`.

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

Also the `SessionStart` hook no longer fires for subagents, this matches
behavior with other coding agents' implementation

# Stack

1. This PR: add `SubagentStart`.
2. #22873: add `SubagentStop`.
3. #22882: add subagent identity to normal hook inputs.
This commit is contained in:
Abhinav
2026-05-19 12:45:08 -07:00
committed by GitHub
Unverified
parent d269aa2af9
commit d661ab70ed
43 changed files with 716 additions and 44 deletions
+2 -1
View File
@@ -49,6 +49,7 @@ pub(crate) fn select_handlers_for_matcher_inputs(
| HookEventName::PermissionRequest
| HookEventName::PostToolUse
| HookEventName::SessionStart
| HookEventName::SubagentStart
| HookEventName::PreCompact
| HookEventName::PostCompact => {
if matcher_inputs.is_empty() {
@@ -139,7 +140,7 @@ pub(crate) fn completed_summary(
fn scope_for_event(event_name: HookEventName) -> HookScope {
match event_name {
HookEventName::SessionStart => HookScope::Thread,
HookEventName::SessionStart | HookEventName::SubagentStart => HookScope::Thread,
HookEventName::PreToolUse
| HookEventName::PermissionRequest
| HookEventName::PostToolUse
+1
View File
@@ -70,6 +70,7 @@ impl ConfiguredHandler {
codex_protocol::protocol::HookEventName::PostCompact => "post-compact",
codex_protocol::protocol::HookEventName::SessionStart => "session-start",
codex_protocol::protocol::HookEventName::UserPromptSubmit => "user-prompt-submit",
codex_protocol::protocol::HookEventName::SubagentStart => "subagent-start",
codex_protocol::protocol::HookEventName::Stop => "stop",
}
}
+23 -6
View File
@@ -86,17 +86,34 @@ use crate::schema::PreToolUseDecisionWire;
use crate::schema::PreToolUsePermissionDecisionWire;
use crate::schema::SessionStartCommandOutputWire;
use crate::schema::StopCommandOutputWire;
use crate::schema::SubagentStartCommandOutputWire;
use crate::schema::UserPromptSubmitCommandOutputWire;
pub(crate) fn parse_session_start(stdout: &str) -> Option<SessionStartOutput> {
let wire: SessionStartCommandOutputWire = parse_json(stdout)?;
let additional_context = wire
.hook_specific_output
.and_then(|output| output.additional_context);
Some(SessionStartOutput {
universal: UniversalOutput::from(wire.universal),
Some(session_start_output(
wire.universal,
wire.hook_specific_output,
))
}
pub(crate) fn parse_subagent_start(stdout: &str) -> Option<SessionStartOutput> {
let wire: SubagentStartCommandOutputWire = parse_json(stdout)?;
Some(session_start_output(
wire.universal,
wire.hook_specific_output,
))
}
fn session_start_output(
universal: HookUniversalOutputWire,
hook_specific_output: Option<crate::schema::SessionStartHookSpecificOutputWire>,
) -> SessionStartOutput {
let additional_context = hook_specific_output.and_then(|output| output.additional_context);
SessionStartOutput {
universal: UniversalOutput::from(universal),
additional_context,
})
}
}
pub(crate) fn parse_pre_tool_use(stdout: &str) -> Option<PreToolUseOutput> {
@@ -16,6 +16,8 @@ pub(crate) struct GeneratedHookSchemas {
pub pre_compact_command_output: Value,
pub session_start_command_input: Value,
pub session_start_command_output: Value,
pub subagent_start_command_input: Value,
pub subagent_start_command_output: Value,
pub user_prompt_submit_command_input: Value,
pub user_prompt_submit_command_output: Value,
pub stop_command_input: Value,
@@ -73,6 +75,14 @@ pub(crate) fn generated_hook_schemas() -> &'static GeneratedHookSchemas {
"session-start.command.output",
include_str!("../../schema/generated/session-start.command.output.schema.json"),
),
subagent_start_command_input: parse_json_schema(
"subagent-start.command.input",
include_str!("../../schema/generated/subagent-start.command.input.schema.json"),
),
subagent_start_command_output: parse_json_schema(
"subagent-start.command.output",
include_str!("../../schema/generated/subagent-start.command.output.schema.json"),
),
user_prompt_submit_command_input: parse_json_schema(
"user-prompt-submit.command.input",
include_str!("../../schema/generated/user-prompt-submit.command.input.schema.json"),
@@ -118,6 +128,8 @@ mod tests {
assert_eq!(schemas.pre_compact_command_output["type"], "object");
assert_eq!(schemas.session_start_command_input["type"], "object");
assert_eq!(schemas.session_start_command_output["type"], "object");
assert_eq!(schemas.subagent_start_command_input["type"], "object");
assert_eq!(schemas.subagent_start_command_output["type"], "object");
assert_eq!(schemas.user_prompt_submit_command_input["type"], "object");
assert_eq!(schemas.user_prompt_submit_command_output["type"], "object");
assert_eq!(schemas.stop_command_input["type"], "object");