S
SessionEnd
HookFires on
SessionEndOnce, as the session closes.
Where this sits in a session
- Session start→
- Prompt submitted→
- Prompt expansion→
- Before tool use→
- After tool use→
- Notification→
- Before compaction→
- Subagent finished→
- Turn finished→
- Session end
Observes only — Emits messages or context. It never returns a permission decision.
What it runs
Executed automatically when the event fires — no prompt, no confirmation.
command
node "${CLAUDE_PLUGIN_ROOT}/hooks/session-end-cleanup.mjs"Install it yourself
Paste into .claude/settings.json to run this hook without installing the whole plugin. Adjust the paths to point at your own copy of the scripts.
settings.json
{
"hooks": {
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-end-cleanup.mjs\""
}
]
}
]
}
}Source
Read this before installing — it runs on your machine with your permissions.
#!/usr/bin/env node
// hooks/src/session-end-cleanup.mts
import { createHash } from "crypto";
import { readdirSync, readFileSync, rmSync, unlinkSync } from "fs";
import { tmpdir } from "os";
import { join, resolve } from "path";
import { fileURLToPath } from "url";
var SAFE_SESSION_ID_RE = /^[a-zA-Z0-9_-]+$/;
function tempSessionIdSegment(sessionId) {
if (SAFE_SESSION_ID_RE.test(sessionId)) {
return sessionId;
}
return createHash("sha256").update(sessionId).digest("hex");
}
function removeFileIfPresent(path) {
try {
unlinkSync(path);
} catch {
}
}
function removeDirIfPresent(path) {
try {
rmSync(path, { recursive: true, force: true });
} catch {
}
}
function parseSessionEndHookInput(raw) {
try {
if (!raw.trim()) return null;
return JSON.parse(raw);
} catch {
return null;
}
}
function normalizeSessionEndSessionId(input) {
if (!input) return null;
const sessionId = input.session_id ?? input.conversation_id ?? "";
return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
}
function parseSessionIdFromStdin() {
return normalizeSessionEndSessionId(parseSessionEndHookInput(readFileSync(0, "utf8")));
}
function main() {
const sessionId = parseSessionIdFromStdin();
if (sessionId === null) {
process.exit(0);
}
const tempRoot = tmpdir();
const prefix = `vercel-plugin-${tempSessionIdSegment(sessionId)}-`;
let entries = [];
try {
entries = readdirSync(tempRoot).filter((name) => name.startsWith(prefix));
} catch {
}
for (const entry of entries) {
const fullPath = join(tempRoot, entry);
if (entry.endsWith(".d")) {
removeDirIfPresent(fullPath);
} else {
removeFileIfPresent(fullPath);
}
}
process.exit(0);
}
var SESSION_END_CLEANUP_ENTRYPOINT = fileURLToPath(import.meta.url);
var isSessionEndCleanupEntrypoint = process.argv[1] ? resolve(process.argv[1]) === SESSION_END_CLEANUP_ENTRYPOINT : false;
if (isSessionEndCleanupEntrypoint) {
main();
}
export {
normalizeSessionEndSessionId,
parseSessionEndHookInput
};
Shipped by 1 plugin
Installing any of these installs this hook.
Reviews
Log in to leave a review.
No reviews yet — be the first.
Explore related
Other things in this space — across every part of the ecosystem, not just hooks.