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
+29 -1
View File
@@ -13,6 +13,7 @@ use codex_hooks::PostToolUseRequest;
use codex_hooks::PreToolUseOutcome;
use codex_hooks::PreToolUseRequest;
use codex_hooks::SessionStartOutcome;
use codex_hooks::StartHookTarget;
use codex_hooks::StopOutcome;
use codex_hooks::StopRequest;
use codex_hooks::UserPromptSubmitOutcome;
@@ -30,6 +31,8 @@ use codex_protocol::protocol::HookRunStatus;
use codex_protocol::protocol::HookRunSummary;
use codex_protocol::protocol::HookSource;
use codex_protocol::protocol::HookStartedEvent;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use serde_json::Value;
use crate::context::ContextualUserFragment;
@@ -100,6 +103,30 @@ pub(crate) async fn run_pending_session_start_hooks(
return false;
};
// Pending session-start hooks are reused to dispatch thread-spawn subagent
// starts. Other subagent sessions are internal/system work and do not run
// start hooks.
let target = match &turn_context.session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_role, .. })
if matches!(
session_start_source,
codex_hooks::SessionStartSource::Startup
) =>
{
let agent_type = agent_role
.clone()
.unwrap_or_else(|| crate::agent::role::DEFAULT_ROLE_NAME.to_string());
StartHookTarget::SubagentStart {
turn_id: turn_context.sub_id.clone(),
agent_id: sess.thread_id().to_string(),
agent_type,
}
}
SessionSource::SubAgent(_) => return false,
_ => StartHookTarget::SessionStart {
source: session_start_source,
},
};
let request = codex_hooks::SessionStartRequest {
session_id: sess.session_id().into(),
#[allow(deprecated)]
@@ -107,7 +134,7 @@ pub(crate) async fn run_pending_session_start_hooks(
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
permission_mode: hook_permission_mode(turn_context),
source: session_start_source,
target,
};
let hooks = sess.hooks();
let preview_runs = hooks.preview_session_start(&request);
@@ -607,6 +634,7 @@ fn hook_run_metric_tags(run: &HookRunSummary) -> [(&'static str, &'static str);
HookEventName::PostCompact => "PostCompact",
HookEventName::SessionStart => "SessionStart",
HookEventName::UserPromptSubmit => "UserPromptSubmit",
HookEventName::SubagentStart => "SubagentStart",
HookEventName::Stop => "Stop",
};
let hook_source = match run.source {