SessionEnd hook killed before completing on exit ("Hook cancelled") — EXIT trap never runs, no configurable grace [2.1.187, tui: fullscreen]

Open 💬 3 comments Opened Jun 24, 2026 by cursed180

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 WorktreeCreate hooks dispatched alongside sibling Agent calls (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

  1. Configure a SessionEnd hook that takes more than a few seconds (e.g. a script doing git fetch/git push, or simply #!/bin/bash + trap 'rm -f /tmp/hook.lock' EXIT; touch /tmp/hook.lock; sleep 30; exit 0).
  2. Start a session, then exit with Ctrl+D (or /exit).
  3. Observe SessionEnd hook [...] failed: Hook cancelled, and that /tmp/hook.lock is left behind — the EXIT trap 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 '...' EXIT lock-release never runs: every subsequent session logs reaped stale lock (>10m old), proving the prior run was killed, not exited. None of the hook's own exit 0 safety 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:

  1. Await SessionEnd hooks before TUI teardown, honoring a per-hook timeout (the timeout field already exists for SessionStart/Stop hooks) — only cancel after it elapses.
  2. If a hook must be cancelled, send SIGTERM first (so trap handlers can run and release locks / clean up) before any SIGKILL.
  3. 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.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗