Summary
On Windows, the codex@openai-codex Claude plugin SessionEnd hook can exceed its configured 5s timeout and Claude reports:
SessionEnd hook [node "${CLAUDE_PLUGIN_ROOT}/scripts/session-lifecycle-hook.mjs" SessionEnd] failed: Hook cancelled
I reproduced this on plugin 1.0.4. In the no-broker teardown path, session-lifecycle-hook.mjs still calls:
handleSessionEnd -> cleanupSessionJobs -> resolveWorkspaceRoot -> ensureGitRepository
resolveWorkspaceRoot delegates to git rev-parse --show-toplevel through runCommand, which uses spawnSync(..., { shell: true }) on Windows. That emits Node DEP0190 and adds shell/git process overhead to every session close.
Local evidence
With realistic SessionEnd hook JSON (hook_event_name, session_id, and cwd) and CLAUDE_PLUGIN_DATA set to the real plugin data dir, the pre-patch hook showed the positive-control DEP0190 on every run:
{
"prePatchElapsedMs": [6205, 4796, 8192],
"prePatchDep0190": [true, true, true]
}
After replacing resolveWorkspaceRoot with a pure fs upward .git walk, the same hook harness completed quickly and emitted no DEP0190:
{
"postPatchElapsedMs": [103, 103, 107],
"postPatchDep0190": [false, false, false]
}
I also compared the full resolveStateDir(cwd) output before and after the patch across 12 cases:
- main checkout and nested subdir
- linked worktree and nested subdir
- junction-accessed worktree and nested subdir
CLAUDE_PLUGIN_DATA set and unset
All state-dir outputs were byte-identical.
Proposed patch
-
In scripts/lib/workspace.mjs, replace resolveWorkspaceRoot(cwd) with a pure fs upward search for a .git file or directory. Return fs.realpathSync.native() for the found root so both the state-dir hash and raw basename slug stay aligned with git rev-parse --show-toplevel.
-
In scripts/lib/broker-lifecycle.mjs, add a local timeout to sendBrokerShutdown(endpoint). A live-but-unresponsive broker should not be able to hold the SessionEnd hook past the configured timeout. A 1500ms socket timeout resolved an intentionally unresponsive named-pipe test in 1516ms.
Notes
- This does not require increasing the hook timeout. Increasing it hides the symptom but also makes Claude block longer on close.
- The broker timeout is not the reproduced no-broker cause; it closes a related live-broker teardown risk.
Summary
On Windows, the
codex@openai-codexClaude pluginSessionEndhook can exceed its configured 5s timeout and Claude reports:I reproduced this on plugin
1.0.4. In the no-broker teardown path,session-lifecycle-hook.mjsstill calls:resolveWorkspaceRootdelegates togit rev-parse --show-toplevelthroughrunCommand, which usesspawnSync(..., { shell: true })on Windows. That emits NodeDEP0190and adds shell/git process overhead to every session close.Local evidence
With realistic
SessionEndhook JSON (hook_event_name,session_id, andcwd) andCLAUDE_PLUGIN_DATAset to the real plugin data dir, the pre-patch hook showed the positive-controlDEP0190on every run:{ "prePatchElapsedMs": [6205, 4796, 8192], "prePatchDep0190": [true, true, true] }After replacing
resolveWorkspaceRootwith a purefsupward.gitwalk, the same hook harness completed quickly and emitted noDEP0190:{ "postPatchElapsedMs": [103, 103, 107], "postPatchDep0190": [false, false, false] }I also compared the full
resolveStateDir(cwd)output before and after the patch across 12 cases:CLAUDE_PLUGIN_DATAset and unsetAll state-dir outputs were byte-identical.
Proposed patch
In
scripts/lib/workspace.mjs, replaceresolveWorkspaceRoot(cwd)with a purefsupward search for a.gitfile or directory. Returnfs.realpathSync.native()for the found root so both the state-dir hash and rawbasenameslug stay aligned withgit rev-parse --show-toplevel.In
scripts/lib/broker-lifecycle.mjs, add a local timeout tosendBrokerShutdown(endpoint). A live-but-unresponsive broker should not be able to hold theSessionEndhook past the configured timeout. A 1500ms socket timeout resolved an intentionally unresponsive named-pipe test in 1516ms.Notes