Repository navigation
[Hooks] SubagentStart and SubagentStop are not dispatched for project-defined custom subagents on Windows #33097
Description
What version of Codex CLI is running?
codex-cli 0.144.1 and codex-cli 0.144.4
What subscription do you have?
Not applicable to this probe (custom Responses-compatible API provider).
Which model were you using?
gpt-5.6-sol (custom Responses-compatible API provider)
What platform is your computer?
Microsoft Windows NT 10.0.19043.0 x64 (Windows 10 Pro, build 19043)
What terminal emulator and version are you using (if applicable)?
Windows PowerShell 5.1.19041.1023
Codex doctor report
{
"status": "not_collected",
"reason": "The completed isolated probes did not collect a Codex doctor report; no additional probe was run."
}What issue are you seeing?
Trusted and active SubagentStart and SubagentStop command hooks are not invoked when a real project-defined custom subagent starts and completes. The custom subagent itself is created successfully and returns the expected result, but the hook handler process is never launched.
This was reproduced in three isolated repositories/runs with Codex CLI 0.144.1 and 0.144.4. The final 0.144.4 run used the wildcard matcher .*, so the result is not explained by the earlier exact matcher.
The current Codex Hooks documentation documents both lifecycle events, says their matcher is applied to the subagent type, and states that project-local hooks load for trusted projects after the hook definitions are reviewed and trusted.
The custom agent starts, returns exactly HOOK_PROBE_WORKER_COMPLETE, and completes, but every real run records:
SubagentStarthandler process launches: 0SubagentStophandler process launches: 0- raw lifecycle payload rows: 0
- hook command errors shown by Codex: none
The handler writes and flushes a process-start record before reading or parsing stdin. Therefore, zero process-start rows means the configured command handler was not launched; this is not merely a payload parsing failure.
Because no real hook payload exists, the opaque runtime agent_id, payload agent_type, parent session_id, transcript_path, and agent_transcript_path are unavailable. The selected project profile is known to be hook_probe_worker, but the payload's runtime agent_type is not inferred or invented.
Codex_Subagent_Hook_Issue_Package.zip
What steps can reproduce the bug?
Sanitized equivalent of the tested configuration
The tested fixture contained both command and commandWindows, with the absolute handler path quoted because the real path contained spaces.
.codex/hooks.json:
{
"hooks": {
"SubagentStart": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "\"C:\\absolute path\\fixture_repo\\hooks\\raw_hook_probe.cmd\" SubagentStart",
"commandWindows": "\"C:\\absolute path\\fixture_repo\\hooks\\raw_hook_probe.cmd\" SubagentStart",
"timeout": 10
}
]
}
],
"SubagentStop": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "\"C:\\absolute path\\fixture_repo\\hooks\\raw_hook_probe.cmd\" SubagentStop",
"commandWindows": "\"C:\\absolute path\\fixture_repo\\hooks\\raw_hook_probe.cmd\" SubagentStop",
"timeout": 10
}
]
}
]
}
}.codex/agents/hook_probe_worker.toml:
name = "hook_probe_worker"
description = "Read-only native lifecycle Hook probe."
sandbox_mode = "read-only"
developer_instructions = """
Do not read any file.
Do not execute any command.
Do not write any file.
Do not create or invoke another agent.
Do not call any tool.
Return exactly this single line and nothing else:
HOOK_PROBE_WORKER_COMPLETE
"""The attached raw_hook_probe.py writes handler_process_start.jsonl before it reads stdin, then records the raw input and JSON parse status in raw_hook_invocations.jsonl. The .cmd wrapper invokes that script with the event name.
Reproduction steps
- Create an isolated Git repository on Windows.
- Add the custom agent, lifecycle hooks, and recorder from the sanitized fixture.
- Replace the placeholder with the quoted absolute path to
raw_hook_probe.cmd. - Start Codex from the repository root with an isolated
CODEX_HOME. - Trust the repository.
- Review and trust both hooks in
/hooks. - Confirm both hooks are active and have no pending review.
- Start exactly one project custom agent of type
hook_probe_worker. - Wait for it to return
HOOK_PROBE_WORKER_COMPLETE. - Check
evidence/real/handler_process_start.jsonlandevidence/real/raw_hook_invocations.jsonl.
Do not pre-create the two evidence files. If the handler launches, it creates them itself.
What is the expected behavior?
When hook_probe_worker starts, Codex should launch the matching SubagentStart command hook and send the lifecycle JSON object to its stdin.
When the same subagent completes, Codex should launch the matching SubagentStop command hook and send the corresponding lifecycle JSON object to its stdin.
Additional information
Environment
- OS: Microsoft Windows 10 Pro, 64-bit, version 10.0.19043 (build 19043)
- Shell: Windows PowerShell 5.1.19041.1023
- Codex CLI versions tested: 0.144.1 and 0.144.4
- 0.144.4 executable SHA-256:
51398051C2332B6AFE08DC3B9DBB4056085C197F35CA57A307EE303D450CADA5 - Model:
gpt-5.6-sol - Model provider: custom Responses-compatible API provider
- Custom agent profile:
hook_probe_worker - Hook source: project-local
.codex/hooks.json - Repository trust: confirmed in the UI
- Hook trust: both hooks manually reviewed, trusted, and shown as active
- Windows sandbox: initialized successfully
The custom provider may be relevant, but custom-agent creation and completion worked normally in every run. No claim is made that the provider is or is not causal.
Reproduction matrix
| Codex version | Matcher | Agent completed | SubagentStart | SubagentStop | Handler launches |
|---|---|---|---|---|---|
| 0.144.1 | ^hook_probe_worker$ |
Yes | No | No | 0 |
| 0.144.4 | ^hook_probe_worker$ |
Yes | No | No | 0 |
| 0.144.4 | .* |
Yes | No | No | 0 |
Each row represents exactly one real custom-agent run. No additional agent was started or resumed in a run.
Controls and causes already checked
- The custom agent was selected and returned the exact expected result.
- The repository and both hook definitions were trusted, and the hooks were shown as active.
- The 0.144.4 wildcard run rules out the tested exact matcher as the explanation.
- The command used a quoted absolute Windows path.
- The same
.cmdwrapper and Python recorder launch successfully with controlled synthetic lifecycle input. - The recorder logs process startup before stdin parsing, so malformed or missing JSON would still produce a process-start row.
- The real runs showed no hook launch error.
- The probes were isolated from the production project integration.
Current diagnosis boundary
The first unobserved boundary is upstream of the handler process. The evidence narrows the issue to one of these areas but does not distinguish between them:
- native subagent lifecycle event generation/dispatch; or
- loading or inheritance of trusted project-local lifecycle hooks for the custom-agent runtime.
The evidence does not establish a source-level root cause, and it does not establish that the custom model provider is irrelevant.
The unresolved boundary is runtime lifecycle dispatch or project-hook inheritance/loading.
Related but not duplicate: #21753 is an umbrella Hook parity tracker; #26383 reports all repo hooks missing under codex exec 0.137.0 on macOS. This report is specifically about trusted and Active custom-subagent lifecycle hooks in the interactive native Windows CLI across 0.144.1 and 0.144.4.
Questions
- Are
SubagentStartandSubagentStopexpected to run for project-defined custom subagents on native Windows Codex CLI? - Should trusted project-local hooks participate in custom-subagent lifecycle dispatch?
- Is lifecycle hook dispatch intended to be independent of the configured model provider?
- Is there a supported runtime trace or developer logging option that shows event generation, matcher evaluation, and hook loading for these events?
Sanitized attachments
- minimal wildcard fixture (
.codex/hooks.json, custom-agent TOML,.cmdwrapper, and Python recorder) - three-run comparison
- result summaries and SHA-256 references for the original local evidence archives
- no API keys, authentication files, cookies, sessions, transcripts,
CODEX_HOME, or unrelated project files
Attachment SHA-256: BC900DA45B9C57B0EE1E2CF750FF7B665D11533921791B0FBB749CD3FF6AA1E7.
Activity
abhinav-oai commentedon Jul 14, 2026
Thanks for the detailed report and the excellent reproduction package. The issue here is that the native Windows environment uses PowerShell to run
commandWindows; it is not necessarily run throughcmd.exe. PowerShell needs the&invocation operator when calling a quoted path with spaces.Could you update the two hooks as follows?
Then re-trust the updated hooks with
/hooksand retry the reproduction. The.cmdwrapper itself can remain unchanged.We’ll improve the documentation around which shell interprets Windows hook commands, since this distinction is easy to miss. Thanks again for flagging it!
GordonFP commentedon Aug 4, 2026
Cross-platform adjacent reproduction and config-axis isolation on macOS.
Environment:
--dangerously-bypass-hook-trust, while user config was ignoredObserved A/B result:
SubagentStart = 1,SubagentStop = 1task_name-only subagent:SubagentStart = 1,SubagentStop = 1SubagentStart = 0,SubagentStop = 0Config-axis isolation:
name+description): both hooks fireddeveloper_instructionsonly: neither hook firedAll custom agents started, returned the requested marker, and completed normally. The recorder writes a process-start row before parsing stdin, so zero rows means no executable handler was registered/launched.
After source inspection, this macOS variant has a narrower likely cause than lifecycle dispatch or Hook-definition loss:
reload::build_next_configincodex-rs/core/src/agent/role.rs.Config::bypass_hook_trustis runtime-only and comes fromConfigOverrides.role.rs::reload_overridesdoes not preservebypass_hook_trust;..Default::default()resets it tofalse.Candidate minimal fix:
in
role.rs::reload_overrides, plus a unit test that applies a non-empty role and asserts the runtime override remains true, and an integration test asserting one start and one stop invocation.I validated the minimal source fix locally against current official
main: a new unit regression test failed before the change because applying the role changedbypass_hook_trustfromtruetofalse; after adding the line above, the same test passed (1 passed, 0 failed). This proves the config-state loss and the narrow field-preservation fix at unit level. A full lifecycle integration test is still recommended before merge.Immediate workaround for users: define the Hook in project/user configuration and persistently review/trust it through
/hooks, instead of relying on invocation-only bypass. A metadata-only role also avoids the rebuild, but loses role-specific runtime settings.Scope clarification: this does not prove the Windows report has the same root cause. The original fixture used persisted project Hook trust, so the PowerShell
&explanation may still apply there. This result establishes a separate cross-platform named-role/trust-inheritance defect with the same visible symptom.vedmalex commentedon Aug 6, 2026
Independent confirmation on macOS, Codex CLI 0.146.1 — same root cause as @GordonFP's 2026-08-04 comment above.
Setup: a project-local
.codex/hooks.jsonwithSubagentStop(matcher: "", wildcard) plus a custom agent TOML that sets bothmodelandsandbox_mode. The framework this feeds is a DA-review gate: a spawned reviewer subagent must return a verdict, and the parent captures it viaSubagentStop.Symptom:
spawn_agent(agent_type="<custom-role>", fork_turns="none")completes normally (the subagent's own session transcript shows a well-formed final message), but the configuredSubagentStopcommand handler is never launched — confirmed with an unconditional trace statement at the very top of our own handler process (before any argument parsing), across multiple runs: zero invocations. The samehooks.json'sStophandler (root session) fires correctly in the same run.Diagnosis matches exactly: the custom agent role carries
model/sandbox_mode, which triggerscore/src/agent/role.rs'sapply_role_to_config→reload::build_next_config. That rebuilds the child session'sConfigviaConfigOverrides { .., ..Default::default() }(role.rs::reload_overrides), dropping the runtime-onlybypass_hook_trust(defaults tofalsepercore/src/config/mod.rs). The child session's hooks engine (session/session.rs→hooks/src/engine/discovery.rs) then silently drops the untrustedSubagentStophandler — no error, no warning, no diagnostic of any kind.Also confirmed:
--dangerously-bypass-hook-truston the invoking session does not help here either, since it's the same runtime-only override that gets dropped on the role-triggered rebuild.Workaround (matches what's proposed above): persist hook trust in the config layer stack instead of relying on the session-level flag — write
[hooks.state]entries with each handler'strusted_hashinto~/.codex/config.toml(obtainable non-interactively viacodex app-server'shooks/listRPC, or interactively via/hooks). Trust state in the layer stack survives the role rebuild; the ephemeral CLI flag does not. After applying this,SubagentStopfired correctly and the subagent's verdict was captured end-to-end.+1 for @GordonFP's proposed fix (
bypass_hook_trust: config.bypass_hook_trust.then_some(true)inrole.rs::reload_overrides) — our independent repro matches the exact mechanism, just triggered by a different combination of role-config fields (model+sandbox_moderather than whatever combination was tested there).One severity note beyond the original report: because the failure is completely silent (no hook error, no log line, nothing in
codex execconsole output — child-session hook activity isn't echoed to the parent's terminal output at all), any automation relying onSubagentStopfor a spawned, role-configured agent will silently lose data with no indication anything went wrong. That silence seems worth its own follow-up (e.g. a warning when a hook is skipped due to untrusted state), separate from the config-inheritance fix itself.mattglass commentedon Aug 10, 2026
Independent confirmation on macOS (Intel) with Codex CLI 0.146.0.
I reproduced the custom-agent hook-trust inheritance issue in an isolated trusted repository.
Observed matrix:
SessionStartwith--dangerously-bypass-hook-trust: delivered.SubagentStartwith the same bypass: delivered asagent_type=default.SubagentStartdelivered with the exact hyphenatedagent_type.The custom agent launched and returned successfully in every case. This isolates the failure from custom-agent naming, matcher syntax, project trust, and general hook execution.
Source inspection of
rust-v0.146.0is consistent with the diagnosis already reported here: applying a non-empty custom-agent role rebuilds the child configuration throughrole.rs::reload_overrides, but that rebuild does not preserveconfig.bypass_hook_trust. Persisted hook trust remains in the configuration layer stack and survives the rebuild, which explains the successful workaround.Immediate workaround confirmed: review and persistently trust the hook through
/hooksinstead of relying on--dangerously-bypass-hook-trust.Suggested regression coverage: one end-to-end custom
agent_typecase for bothSubagentStartandSubagentStop, plus a config-level assertion that the invocation-scoped bypass survives the role rebuild.sudo-vaibhav commentedon Aug 23, 2026
bumping this up
fix(codex): namespace-pair tool-call emission; MCP output replace re-…
harness-watch: scheduled harness-version automation + capability cove…