SessionStart hook on resume can report `hook timed out after 60s` even when the hook exits 0 in ~4s
On Windows, Codex CLI resumed a session and showed:
SessionStart hook (failed)error: hook timed out after 60s
For that same resumed session:
- the outer
SessionStartwrapper finishedexit=0in4085ms - the inner hook finished
outcome=successin3218ms - the session was still
runningimmediately afterward
This looks like a false or misattributed SessionStart timeout banner on the
resume path rather than a real 60s timeout of the successful hook invocation.
Environment
- Codex CLI:
0.128.0 - OS: Windows 10
- Shell: PowerShell 7
- Terminal emulator: Windows Terminal
[features].codex_hooks = true[features].tui_app_server = true- Hook type:
SessionStartcommand hook - Configured
SessionStarttimeout:60
Hook Setup
The specimen was taken with a SessionStart command hook configured attimeout: 60.
The hook ran through a PowerShell wrapper that:
- logged start/end timing
- synchronously invoked the real hook work
- exited with the real hook result
Sanitized shape:
config.toml
[features]
codex_hooks = true
tui_app_server = true
hooks.json
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "& \"<sessionstart-wrapper>.ps1\"",
"timeout": 60
}
]
}
]
}
}
Observed
On resume, Codex displayed the failure banner above.
For the same resumed session id, local timing showed:
phase=start ... sessionId=<resumed-session-id> source=resume
phase=end ... sessionId=<resumed-session-id> source=resume exit=0 elapsedMs=4085
For that same resumed session id, the inner hook log showed:
event=SessionStart outcome=success ... sessionId=<resumed-session-id> ... totalMs=3218
Immediately afterward, a direct status check for that same resumed session id
returned:
{
"found": true,
"sessionId": "<resumed-session-id>",
"state": "running"
}
Minimal Repro Shape
I do not yet have a tiny standalone repro outside my normal setup, but the
observed shape is:
- Enable a
SessionStartcommand hook on Windows. - Set
timeoutto60. - Run the hook through a wrapper that logs timing and then invokes the real
hook synchronously.
- Resume an existing Codex session.
- Observe that Codex may display:
SessionStart hook (failed)error: hook timed out after 60s
- Compare the banner against hook-side timing for the same resumed session id.
In this specimen, hook-side timing contradicted the UI banner.
Expected
- If the hook really times out, the same resumed-session invocation should not
later show successful completion.
- If the hook completes successfully well inside the timeout window, Codex
should not show a timeout failure banner for that resumed session.
Evidence
1. User-visible failure
The terminal UI displayed:
SessionStart hook (failed)
error: hook timed out after 60s
2. Outer wrapper completed successfully for the resumed session
phase=start ... sessionId=<resumed-session-id> source=resume
phase=end ... sessionId=<resumed-session-id> source=resume exit=0 elapsedMs=4085
3. Inner hook completed successfully for the same resumed session
event=SessionStart outcome=success ... sessionId=<resumed-session-id> ... totalMs=3218
4. The resumed session was still running afterward
{
"found": true,
"sessionId": "<resumed-session-id>",
"state": "running"
}
Possible Clue
Secondary clue only, not required for the main claim:
- A separate startup-path wrapper invocation on a different internal session id
exited quickly with code 1.
- That may explain the wrong banner attribution if Codex is mixing startup-path
and resume-path hook outcomes internally.
Suspected Issue
Codex may be surfacing stale SessionStart failure state on resume, or
attributing a different startup-path hook outcome to the resumed session UI.
2 Comments
Final validation against the normal
artifacts\cli\currentbuild also passed cleanly on the same first-post-boot repro path.---
Apologies for the noise and any inconvenience caused while we investigated this. After digging further, this appears to have been a local issue in our
codex-statehelper chain rather than a Codex bug.We were able to identify the root cause and reproduce/fix it locally on Windows.
Following up on the original report: based on the first evidence we had, this initially looked like a Codex-side false timeout or wrong-attribution problem on the resume path. After adding deeper instrumentation and expanding the repro set, that conclusion no longer appears correct.
Investigation summary
Our investigation ended up happening in two stages:
More concretely, we:
stdout/stderrEOF after the child had already exited0, andCodex was timing out later in the drain phase
codex-statewrapper chain and broker startupartifacts\cli\currentbuild and reran the original cold reboot repro successfullyRoot cause
The issue was not really a
SessionStart-specific problem.SessionStartwas usually just the first event in the chain after reboot, so it was the first code path that touched our localcodex-statebroker and exposed the bug.On cold startup, our
codex-statebroker auto-start path on Windows launched the long-lived broker withUseShellExecute = false. That allowed transient hook pipe handles from the triggering Codex hook process tree to leak into the broker process tree.In simple terms:
What Codex then saw was:
0in about4sto5sstdout/stderrnever reached EOF60stimeoutThat is why it looked like a
SessionStartfreeze even though the actual hook work had already succeeded.What we tested
We used a locally patched Codex build to add more detailed hook-runner logging so we could distinguish:
stdout/stderrdrain after the child had already exitedThe important failing cold-start specimens showed:
child_exitedfirststream_drain_timeoutsuspected_inherited_handle0bytes ever read on bothstdoutandstderr<img width="695" height="185" alt="Image" src="https://github.com/user-attachments/assets/178e2cbb-a65d-4860-9d40-e0f152fc68a9" />
We then tested two local
codex-statefixes:The stdio-redirection-only change was not sufficient.
The detached Windows broker launch cleared the first-post-boot repro on the same machine. After that change:
SessionStartwindowSessionStart hook completed ... status=CompletedWe then promoted that fix into the normal published
artifacts\cli\currentbuild and reran the same cold reboot test. That final integrated run was also clean, which gave us confidence that the real shared fix path was correct and that we were not only proving the behavior of a one-off experiment build.Fix
We patched our local
codex-statebroker auto-start path on Windows so the background broker no longer inherits the transient hook pipe handles from the triggering Codex hook process tree.No change was needed to
SessionStartevent semantics themselves, and no change was needed to the broker's actual IPC model; it still communicates over its named pipe as before.Warning For Hook Wrapper Authors
If you build wrappers around Codex hooks on Windows, be careful about launching long-lived background children from inside the hook process tree.
In particular, avoid:
stdout/stderror other inheritable handles from the hook chainThat can create a false-positive hook timeout where:
0The safe pattern is:
Important clarification
This was likely not specific to
SessionStartin principle.SessionStartwas just the first event most likely to hit the cold broker-start path after reboot. Other first-touch broker events could have exposed the same underlying issue.TL;DR
codex-statebroker startup on Windows, not CodexSessionStartitself60stimeout happened: the hook child exited, but our broker kept inherited hook pipes open, so Codex waited for EOF and timed out during drainSessionStart: that was usually the first event to touch the broker after bootWe’ll close this from our side unless the extra hook-runner diagnostics are still useful upstream. Those diagnostics were helpful in proving that this was a
drain_after_exit/ EOF problem rather than the hook process still running.Indexed this hook ticket in the umbrella tracker: #21753
Goal: collect the scattered Codex hook requests and bugs into one parity matrix for Full Claude Code Hook Parity (29+), while preserving this issue as the detailed thread for its specific behavior.