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.
-
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.
-
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.
-
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
- Install ECC, let
session-summary.js be active in the Stop hook chain (default installation triggers this on certain project names).
- Start a short Claude Code session in project A. End it normally.
- Start another short session in the same project. End it.
- Inspect
~/.claude/sessions/{today}-{shortId}-session.tmp.
- Expected:
### Tasks contains several real user messages from the latest session; Total user messages > 1.
- 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
Summary
scripts/hooks/session-end.jsproduces corrupted*-session.tmpfiles when the Stop hook chain includes another hook (e.g.scripts/csm/session-summary.js) that spawnsclaude -p ...as a subprocess. Because the subprocess also fires the Stop hook chain,session-end.jsis 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.tmpfor certain projects looks like:Examples observed:
~/.claude/sessions/2026-04-19-obsidian-session.tmp~/.claude/sessions/2026-04-18-obsidian-session.tmpHealthy 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.
scripts/lib/utils.jsgetSessionIdShort()falls back to the project name whenCLAUDE_SESSION_IDis not set:→ All sessions for the same project share the same filename.
scripts/csm/session-summary.js(a Stop-hook script registered insettings.json) callsspawnSync('claude', ['-p', prompt], ...)to generate an AI summary. The subprocess also runs the full Stop hook chain, includingsession-end.js.In
scripts/hooks/session-end.js(lines ~191-194):Both the parent and the subprocess compute the same path. The subprocess transcript contains only the summarization prompt, so its
### Tasksends up holding the prompt text andTotal user messages: 1. The parent's valid summary is overwritten.Proposed fix (A + B, complementary)
A. Derive
shortIdfromtranscript_pathwhen availableIn
session-end.js, extract the first 8 hex chars of the session UUID from the transcript filename instead of relying solely ongetSessionIdShort():This guarantees parent vs. subprocess map to different files because each session has a distinct UUID.
Trade-off: it increases the number of
.tmpfiles 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
.tmpfiles from AI-summary subprocesses at all, pass an env flag when spawning the child and skipsession-end.jswhen it is set.In
scripts/csm/session-summary.js:At the top of
session-end.js#main: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
session-summary.jsbe active in the Stop hook chain (default installation triggers this on certain project names).~/.claude/sessions/{today}-{shortId}-session.tmp.### Taskscontains several real user messages from the latest session;Total user messages> 1.### Taskscontains the summarization prompt;Total user messages: 1.Environment
~/.claude/scripts/(synced frommainon 2026-04-19)Notes
session-start.js). This issue is about the write side (session-end.js).