SessionEnd hook killed before completing on exit ("Hook cancelled") — EXIT trap never runs, no configurable grace [2.1.187, tui: fullscreen]
Summary
On session exit (Ctrl+D / /exit), a long-running SessionEnd hook is terminated before it can finish and reported as SessionEnd hook [...] failed: Hook cancelled. The termination is abrupt — the hook's shell EXIT/trap handlers do not run, so it leaves partial state behind (in my case a lock directory that is never released). There appears to be no grace window long enough for a multi-second hook, and no way to configure one. With the fullscreen TUI (tui: "fullscreen", the "reduce screen flicker" option) this surfaces as a visible error on every exit; the underlying kill predates the visible message.
Related but distinct from:
- #63495 (closed/fixed) — a spurious "Hook cancelled" for a hook that exits 0 immediately (purely a display artifact). My slow hook genuinely never completes, so this is not the same display-only bug.
- #62422 (open) — "Hook cancelled" for
WorktreeCreatehooks dispatched alongside siblingAgentcalls (different trigger, same message class).
Environment
- Claude Code: 2.1.187
- OS: Windows 11, Git Bash hooks
"tui": "fullscreen"- The hook does network I/O (
git fetch+git push); on this machine a security suite's TLS-inspection proxy + on-write file scanning push its wall-clock to ~3.5 min.
Reproduction
- Configure a
SessionEndhook that takes more than a few seconds (e.g. a script doinggit fetch/git push, or simply#!/bin/bash+trap 'rm -f /tmp/hook.lock' EXIT; touch /tmp/hook.lock; sleep 30; exit 0). - Start a session, then exit with
Ctrl+D(or/exit). - Observe
SessionEnd hook [...] failed: Hook cancelled, and that/tmp/hook.lockis left behind — theEXITtrap never ran.
Evidence (my case)
time <hook>(dry-run, no push) =real 3m36s,user 9s— almost entirely I/O wait. No plausible teardown grace covers this.- The hook's
trap '...' EXITlock-release never runs: every subsequent session logsreaped stale lock (>10m old), proving the prior run was killed, not exited. None of the hook's ownexit 0safety paths are reached. - Net effect: the SessionEnd work silently stopped succeeding for 3 days before the (newly visible) message made it noticeable.
Expected behavior
At least one of:
- Await
SessionEndhooks before TUI teardown, honoring a per-hooktimeout(thetimeoutfield already exists forSessionStart/Stophooks) — only cancel after it elapses. - If a hook must be cancelled, send SIGTERM first (so
traphandlers can run and release locks / clean up) before any SIGKILL. - Document the SessionEnd-on-exit grace/await semantics, and whether SessionEnd hooks run sequentially or in parallel — in my 2-hook batch a fast second hook (
exit 0, sub-second) also reports "Hook cancelled", which suggests sequential execution where the slow first hook starves the whole window.
Workaround
Move slow work off SessionEnd to an external scheduler (I moved a memory-sync hook to a 30-min OS scheduled task). But "must-complete" and "fire-and-forget" SessionEnd hooks are a legitimate pattern (cf. #63495), so first-class support / clear semantics would help.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗