Skip to content

session-end.js: *-session.tmp corruption from subprocess spawn in Stop hook chain #1494

@ratorin

Description

@ratorin

Summary

scripts/hooks/session-end.js produces corrupted *-session.tmp files when the Stop hook chain includes another hook (e.g. scripts/csm/session-summary.js) that spawns claude -p ... as a subprocess. Because the subprocess also fires the Stop hook chain, session-end.js is invoked twice with the same computed filename, and the subprocess overwrites the parent's valid summary with a summary of the AI-summarization prompt itself.

Symptom

After a few sessions, ~/.claude/sessions/{date}-{shortId}-session.tmp for certain projects looks like:

### Tasks
- 以下はClaude Codeセッションの会話ログ抜粋です。日本語3行で簡潔に要約してください。  各行の形式: 1行目: 主なタスク・目的 ...

### Stats
- Total user messages: 1

Examples observed:

  • ~/.claude/sessions/2026-04-19-obsidian-session.tmp
  • ~/.claude/sessions/2026-04-18-obsidian-session.tmp

Healthy files (e.g. for actively developed projects) show many real user messages and correct stats.

Root cause

Filename collision between the parent session and a subprocess spawned within the same Stop hook chain.

  1. scripts/lib/utils.js getSessionIdShort() falls back to the project name when CLAUDE_SESSION_ID is not set:

    function getSessionIdShort(fallback = 'default') {
      const sessionId = process.env.CLAUDE_SESSION_ID;
      if (sessionId && sessionId.length > 0) {
        const sanitized = sanitizeSessionId(sessionId.slice(-8));
        if (sanitized) return sanitized;
      }
      return sanitizeSessionId(getProjectName()) || sanitizeSessionId(fallback) || 'default';
    }

    → All sessions for the same project share the same filename.

  2. scripts/csm/session-summary.js (a Stop-hook script registered in settings.json) calls spawnSync('claude', ['-p', prompt], ...) to generate an AI summary. The subprocess also runs the full Stop hook chain, including session-end.js.

  3. In scripts/hooks/session-end.js (lines ~191-194):

    const sessionsDir = getSessionsDir();
    const today = getDateString();
    const shortId = getSessionIdShort();
    const sessionFile = path.join(sessionsDir, `${today}-${shortId}-session.tmp`);

    Both the parent and the subprocess compute the same path. The subprocess transcript contains only the summarization prompt, so its ### Tasks ends up holding the prompt text and Total user messages: 1. The parent's valid summary is overwritten.

Proposed fix (A + B, complementary)

A. Derive shortId from transcript_path when available

In session-end.js, extract the first 8 hex chars of the session UUID from the transcript filename instead of relying solely on getSessionIdShort():

let shortId = null;
if (transcriptPath) {
  const m = path.basename(transcriptPath).match(/([0-9a-f]{8})-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/i);
  if (m) shortId = m[1];
}
if (!shortId) shortId = getSessionIdShort(); // keep existing fallback
const sessionFile = path.join(sessionsDir, `${today}-${shortId}-session.tmp`);

This guarantees parent vs. subprocess map to different files because each session has a distinct UUID.

Trade-off: it increases the number of .tmp files in ~/.claude/sessions/. Any SessionStart-hook logic that picks "the latest session for this project" should fall back to mtime ordering rather than filename equality. (Relevant PRs #1481, #1485 already tighten the matching logic on the read side, which is compatible with this change.)

B. Subprocess identification flag (additive)

To avoid creating stray .tmp files from AI-summary subprocesses at all, pass an env flag when spawning the child and skip session-end.js when it is set.

In scripts/csm/session-summary.js:

env: Object.assign({}, process.env, { ECC_SUMMARY_SUBPROCESS: '1' })

At the top of session-end.js#main:

if (process.env.ECC_SUMMARY_SUBPROCESS === '1') process.exit(0);

A alone stops the corruption. B alone also stops it while keeping the session directory clean. Using both is safe and gives the most predictable behaviour.

Reproduction

  1. Install ECC, let session-summary.js be active in the Stop hook chain (default installation triggers this on certain project names).
  2. Start a short Claude Code session in project A. End it normally.
  3. Start another short session in the same project. End it.
  4. Inspect ~/.claude/sessions/{today}-{shortId}-session.tmp.
  5. Expected: ### Tasks contains several real user messages from the latest session; Total user messages > 1.
  6. Observed: ### Tasks contains the summarization prompt; Total user messages: 1.

Environment

  • OS: Windows 11 (the bug is platform-independent — same logic applies to macOS/Linux)
  • Node: v20+
  • Claude Code: v2.1.113
  • ECC: cached plugin v1.8.0 + running against scripts in ~/.claude/scripts/ (synced from main on 2026-04-19)

Notes

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