From c40b883413cf10146ce0d78b18592134b8f2c101 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Sun, 12 Apr 2026 22:13:16 -0700 Subject: [PATCH] fix: prefer repo-relative hook file paths --- scripts/hooks/session-activity-tracker.js | 35 +++++++------ tests/hooks/session-activity-tracker.test.js | 52 ++++++++++++++++++++ 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/scripts/hooks/session-activity-tracker.js b/scripts/hooks/session-activity-tracker.js index 5ff85d35..19f9d9a8 100644 --- a/scripts/hooks/session-activity-tracker.js +++ b/scripts/hooks/session-activity-tracker.js @@ -359,17 +359,10 @@ function gitRepoRoot(cwd) { return runGit(['rev-parse', '--show-toplevel'], cwd); } +const MAX_RELEVANT_PATCH_LINES = 6; + function candidateGitPaths(repoRoot, filePath) { const resolvedRepoRoot = path.resolve(repoRoot); - const absolute = path.isAbsolute(filePath) - ? path.resolve(filePath) - : path.resolve(process.cwd(), filePath); - const relative = path.relative(resolvedRepoRoot, absolute); - - if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) { - return []; - } - const candidates = []; const pushCandidate = value => { const candidate = String(value || '').trim(); @@ -379,10 +372,24 @@ function candidateGitPaths(repoRoot, filePath) { candidates.push(candidate); }; - pushCandidate(relative); - pushCandidate(relative.split(path.sep).join('/')); - pushCandidate(absolute); - pushCandidate(absolute.split(path.sep).join('/')); + const absoluteCandidates = path.isAbsolute(filePath) + ? [path.resolve(filePath)] + : [ + path.resolve(resolvedRepoRoot, filePath), + path.resolve(process.cwd(), filePath), + ]; + + for (const absolute of absoluteCandidates) { + const relative = path.relative(resolvedRepoRoot, absolute); + if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) { + continue; + } + + pushCandidate(relative); + pushCandidate(relative.split(path.sep).join('/')); + pushCandidate(absolute); + pushCandidate(absolute.split(path.sep).join('/')); + } return candidates; } @@ -404,7 +411,7 @@ function patchPreviewFromGitDiff(repoRoot, pathCandidates) { || (line.startsWith('+') && !line.startsWith('+++')) || (line.startsWith('-') && !line.startsWith('---')) ) - .slice(0, 6); + .slice(0, MAX_RELEVANT_PATCH_LINES); if (relevant.length > 0) { return relevant.join('\n'); diff --git a/tests/hooks/session-activity-tracker.test.js b/tests/hooks/session-activity-tracker.test.js index 99eb7c6f..3eb95a02 100644 --- a/tests/hooks/session-activity-tracker.test.js +++ b/tests/hooks/session-activity-tracker.test.js @@ -309,6 +309,58 @@ function runTests() { fs.rmSync(repoDir, { recursive: true, force: true }); }) ? passed++ : failed++); + (test('resolves repo-relative paths even when the hook runs from a nested cwd', () => { + const tmpHome = makeTempDir(); + const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'session-activity-tracker-nested-repo-')); + + spawnSync('git', ['init'], { cwd: repoDir, encoding: 'utf8' }); + spawnSync('git', ['config', 'user.email', 'ecc@example.com'], { cwd: repoDir, encoding: 'utf8' }); + spawnSync('git', ['config', 'user.name', 'ECC Tests'], { cwd: repoDir, encoding: 'utf8' }); + + const srcDir = path.join(repoDir, 'src'); + const nestedCwd = path.join(repoDir, 'subdir'); + fs.mkdirSync(srcDir, { recursive: true }); + fs.mkdirSync(nestedCwd, { recursive: true }); + + const trackedFile = path.join(srcDir, 'app.ts'); + fs.writeFileSync(trackedFile, 'const count = 1;\n', 'utf8'); + spawnSync('git', ['add', 'src/app.ts'], { cwd: repoDir, encoding: 'utf8' }); + spawnSync('git', ['commit', '-m', 'init'], { cwd: repoDir, encoding: 'utf8' }); + + fs.writeFileSync(trackedFile, 'const count = 2;\n', 'utf8'); + + const input = { + tool_name: 'Write', + tool_input: { + file_path: 'src/app.ts', + content: 'const count = 2;\n', + }, + tool_output: { output: 'updated src/app.ts' }, + }; + const result = runScript(input, { + ...withTempHome(tmpHome), + CLAUDE_HOOK_EVENT_NAME: 'PostToolUse', + ECC_SESSION_ID: 'ecc-session-nested-cwd', + }, { + cwd: nestedCwd, + }); + assert.strictEqual(result.code, 0); + + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'tool-usage.jsonl'); + const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); + assert.deepStrictEqual(row.file_events, [ + { + path: 'src/app.ts', + action: 'modify', + diff_preview: 'const count = 1; -> const count = 2;', + patch_preview: '@@ -1 +1 @@\n-const count = 1;\n+const count = 2;', + }, + ]); + + fs.rmSync(tmpHome, { recursive: true, force: true }); + fs.rmSync(repoDir, { recursive: true, force: true }); + }) ? passed++ : failed++); + (test('prefers ECC_SESSION_ID over CLAUDE_SESSION_ID and redacts bash summaries', () => { const tmpHome = makeTempDir(); const input = {