Skip to content

SessionEnd hook can hit 5s timeout on Windows due to shelling out to git #403

Description

@dzhokn

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

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions