ClaudeSuperPower
S

SessionEnd

Hook

Fires on

SessionEnd

Once, as the session closes.

Where this sits in a session

  1. Session start
  2. Prompt submitted
  3. Prompt expansion
  4. Before tool use
  5. After tool use
  6. Notification
  7. Before compaction
  8. Subagent finished
  9. Turn finished
  10. Session end

Observes onlyEmits 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.