From a35b2d125d6e2177298748167d424f0a441a06ee Mon Sep 17 00:00:00 2001 From: Taro Kawakami Date: Sun, 19 Apr 2026 11:37:32 +0900 Subject: [PATCH] fix(hooks): isolate session-end.js filename using transcript_path UUID When session-end.js runs and CLAUDE_SESSION_ID is unset, getSessionIdShort() falls back to the project/worktree name. If any other Stop-hook in the chain spawns a claude subprocess (e.g. an AI-summary generator using 'claude -p'), the subprocess also fires the full Stop chain and writes to the same project- name-based filename, clobbering the parent's valid session summary with a summary of the summarization prompt itself. Fix: when stdin JSON (or CLAUDE_TRANSCRIPT_PATH) provides a transcript_path, extract the first 8 hex chars of the session UUID from the filename and use that as shortId. Falls back to the original getSessionIdShort() when no transcript_path is available, so existing behavior is preserved for all callers that do not set it. Adds a regression test in tests/hooks/hooks.test.js. Refs #1494 --- scripts/hooks/session-end.js | 11 ++++++++++- tests/hooks/hooks.test.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/scripts/hooks/session-end.js b/scripts/hooks/session-end.js index af378001..3c1ec0ac 100644 --- a/scripts/hooks/session-end.js +++ b/scripts/hooks/session-end.js @@ -190,7 +190,16 @@ async function main() { const sessionsDir = getSessionsDir(); const today = getDateString(); - const shortId = getSessionIdShort(); + // Prefer the real session UUID (first 8 chars) from transcript_path when available. + // Without this, a parent session and any `claude -p ...` subprocess spawned by + // another Stop-hook share the project-name fallback filename, and the subprocess + // overwrites the parent's summary. See issue #1494 for full repro details. + 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(); } const sessionFile = path.join(sessionsDir, `${today}-${shortId}-session.tmp`); const sessionMetadata = getSessionMetadata(); diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index 78664fa6..2c876404 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -633,6 +633,40 @@ async function runTests() { passed++; else failed++; + // Regression test for #1494: transcript_path UUID takes precedence over fallback + if ( + await asyncTest('derives shortId from transcript_path UUID when available', async () => { + const isoHome = path.join(os.tmpdir(), `ecc-session-transcript-${Date.now()}`); + const transcriptUuid = 'abcdef12-3456-4789-a012-bcdef3456789'; + const expectedShortId = 'abcdef12'; // First 8 chars of UUID + const transcriptPath = path.join(isoHome, 'transcripts', `${transcriptUuid}.jsonl`); + + try { + fs.mkdirSync(path.dirname(transcriptPath), { recursive: true }); + fs.writeFileSync(transcriptPath, ''); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson, { + HOME: isoHome, + USERPROFILE: isoHome, + // CLAUDE_SESSION_ID intentionally unset so that without the fix the project-name + // fallback would be used, exposing the filename collision described in #1494. + }); + + const sessionsDir = getCanonicalSessionsDir(isoHome); + const now = new Date(); + const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; + const sessionFile = path.join(sessionsDir, `${today}-${expectedShortId}-session.tmp`); + + assert.ok(fs.existsSync(sessionFile), `Session file with transcript UUID shortId should exist: ${sessionFile}`); + } finally { + fs.rmSync(isoHome, { recursive: true, force: true }); + } + }) + ) + passed++; + else failed++; + if ( await asyncTest('writes project, branch, and worktree metadata into new session files', async () => { const isoHome = path.join(os.tmpdir(), `ecc-session-metadata-${Date.now()}`);