Skip to content

SessionEnd hook reports "Hook cancelled" on exit (1.5s deferred exit outlives shutdown grace) #991

Open

Description

@JaeyoulLee

Summary

The SessionEnd hook intermittently reports Hook cancelled in Claude Code when the user exits a session:

SessionEnd hook [node "${CLAUDE_PLUGIN_ROOT}/scripts/session-end.mjs"] failed: Hook cancelled

Root cause

In src/hooks/session-end.ts (built to scripts/session-end.mjs), the hook fires best-effort POSTs to the local engine and then defers exit:

setTimeout(() => process.exit(0), 1500).unref();

Two problems:

  1. Outlives the shutdown grace. On exit, Claude Code gives SessionEnd hooks only a short grace window. The 1.5s deferred exit (plus any pending fetch socket keeping the event loop alive) means the hook process is still running when the harness tears down → the harness kills it and surfaces Hook cancelled. The session-end telemetry POST usually still goes out, so it's cosmetic — but it's noisy on every exit.

  2. .unref() defeats the intended cap. Because the in-flight fetch sockets keep the loop alive, the unref'd timer is the only thing forcing exit; if the engine is slow/unreachable the process can linger longer than expected before the timer fires.

Reproduction

  • Node 22, agentmemory 0.9.27, Claude Code plugin install.
  • Exit a session (exit). Intermittently shows the Hook cancelled line.

Fix that worked locally

Shorten the deferred exit so the hook quits well inside the harness grace, while still giving the localhost POST time to flush:

setTimeout(() => process.exit(0), 500);

This eliminated the Hook cancelled message in my testing while preserving the fire-and-forget POST. A more robust option would be to exit as soon as the primary /session/end request settles, with a small hard cap.

Minor adjacent note

scripts/session-end.mjs uses global fetch, which is absent on Node < 18 (e.g. Node 12 → ReferenceError: fetch is not defined, unhandled rejection). A typeof fetch === 'function' guard (or an explicit Node version requirement) would fail more gracefully on systems where the hook is spawned under an older node on PATH.

Affects: same pattern likely applies to other hook scripts that use the setTimeout(...).unref() exit idiom.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions