SessionEnd cleanup hook gets cancelled on exit — hooks.json ships no `timeout`, and Claude Code's default for SessionEnd hooks is only 1.5s
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 287
- Forks
- 58
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 17
Description
Problem
On session exit, the plugin's SessionEnd hook is reported as failed on every substantive session close:
SessionEnd hook [node "${CLAUDE_PLUGIN_ROOT}/hooks/session-end-cleanup.mjs"] failed: Hook cancelled
This is not just a display artifact — the cleanup genuinely does not run. The session-scoped dedup temp files (<tmpdir>/vercel-plugin-<sessionId>-*, the claim dir / seen-skills state described in the plugin's own dedup contract) are left behind for closed sessions:
$ ls /tmp | grep vercel-plugin
vercel-plugin-1ec5a71c-...-greenfield.txt # session closed at 22:10 — cleanup never ran
vercel-plugin-1ec5a71c-...-likely-skills.txt
Root cause
hooks/hooks.json declares the SessionEnd hook with no timeout field (current main, blob 3c6562c).
In Claude Code (verified in the 2.1.214 bundle), executeSessionEndHooks invokes the hook runner with a default timeoutMs of 1500 for SessionEnd specifically. Per hook, the budget is hook.timeout * 1000 if configured, else that 1500ms default. A hook that exceeds its budget is aborted (ABORT_ERR → "Hook cancelled").
session-end-cleanup.mjs itself is fast — standalone it exits 0 in ~50ms:
$ echo '{"session_id":"x"}' | node hooks/session-end-cleanup.mjs # 0.047s, exit 0
But at a real session close the 1.5s window is easily consumed before the hook finishes: session teardown (notably with "tui": "fullscreen") plus sibling SessionEnd hooks run in the same instant. anthropics/claude-code#70465 (open) documents this exact class — a sub-second exit 0 SessionEnd hook cancelled alongside busy siblings. In my setup, user-configured SessionEnd hooks that declare explicit timeout values (10–30s) run to completion every close; the plugin's hook — the only SessionEnd hook without a timeout — is the only one that gets cancelled.
There's a second cost of not declaring a timeout: Claude Code derives its SessionEnd force-exit grace from the max configured timeout across all SessionEnd hooks, clamped to [1.5s, 60s]. For users whose only SessionEnd hook is this plugin's, everything collapses to the 1.5s floor.
Suggested fix
Declare a timeout on the SessionEnd hook (same one-field-per-hook shape as the shell fix proposed in #79):
"SessionEnd": [
{
"hooks": [
{
"type": "command",
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-end-cleanup.mjs\""
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-end-cleanup.mjs\"",
+ "timeout": 15
}
]
}
]
This has no latency cost in the normal case — the hook still exits in milliseconds; the timeout only widens the abort window so teardown contention can't kill it. In the same environment, every SessionEnd hook that declares an explicit timeout completes on every close; only this one — the only one relying on the 1.5s default — gets cancelled.
timeout is a documented per-hook field in the hooks reference, and the plugin's own doctor already reasons about hook timeout risk, so this seems like the intended knob.
Environment
- Claude Code 2.1.214,
"tui": "fullscreen" - vercel-plugin 0.44.0 (marketplace install)
- Linux x86_64 (Arch-based)
Related
- anthropics/claude-code#70465 — open: SessionEnd hooks killed on exit, fast siblings included, no configurable grace
- anthropics/claude-code#63495 — closed: the display-only variant (hook exits 0 but message still shown). Distinct from this report: here the leftover temp files prove the cleanup really doesn't run.
- #79 — same file, precedent for adding per-hook fields to
hooks.json
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in hooks/hooks.json and inspect the SessionEnd hook for session-end-cleanup.mjs, then review the hook timeout reference and the precedent in #79. Verify the configured timeout prevents cancellation during session teardown and run the standalone node command from the report to confirm the cleanup hook exits successfully.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- Under an hour
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100