From 7cbe5208f7191a9b14ed00948afab69f576b3c47 Mon Sep 17 00:00:00 2001 From: Mohammad Alsayyedan Date: Sun, 20 Sep 2026 20:31:02 -0500 Subject: [PATCH] Fix workspace name resolution for paths containing underscores Claude Code encodes underscores as hyphens when building the project directory name under ~/.claude/projects, but encodeComponentForMatch only normalized whitespace. Any path component containing an underscore could therefore never match a readdirSync entry, so resolution stopped at the first such level and the fallback used the entire unresolved tail as the workspace name. A user whose home directory is /home/my_name saw every Claude workspace labeled "my-name-AIOps" instead of "AIOps", which also prevents sessions from aggregating with the same project recorded by other harnesses. Normalize underscores alongside whitespace, and add a regression test covering a path component with underscores. --- src/core/parser-claude.test.ts | 33 +++++++++++++++++++++++++++++++++ src/core/parser-claude.ts | 4 ++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/core/parser-claude.test.ts b/src/core/parser-claude.test.ts index 588b33bf..e5c9740c 100644 --- a/src/core/parser-claude.test.ts +++ b/src/core/parser-claude.test.ts @@ -384,6 +384,39 @@ describe('parseClaudeSessions', () => { } }); + // ---- workspace name resolution with underscores ---- + + it('resolves workspace name when path contains a folder with underscores', { timeout: 30_000 }, () => { + // Claude Code encodes underscores as hyphens, so the encoded name cannot be + // matched against readdirSync output unless underscores are normalized too. + const tmpBase = fs.mkdtempSync(path.join(longTmpDir(), 'claude-us-')); + const underscoreParent = path.join(tmpBase, 'my_home_dir'); + const targetDir = path.join(underscoreParent, 'proj'); + fs.mkdirSync(targetDir, { recursive: true }); + + const encodedDirName = targetDir + .replace(/^([a-zA-Z])(?=:)/, d => d.toLowerCase()) + .replace(/[:\\/\s_]/g, '-'); + + const root = fs.mkdtempSync(path.join(longTmpDir(), 'claude-proj-')); + const projectsDir = path.join(root, 'projects'); + const projDir = path.join(projectsDir, encodedDirName); + fs.mkdirSync(projDir, { recursive: true }); + fs.writeFileSync( + path.join(projDir, 's.jsonl'), + [makeUser('hello'), makeAssistant('hi')].map(l => JSON.stringify(l)).join('\n'), + ); + + try { + const result = parseClaudeSessions(projectsDir); + expect(result).toHaveLength(1); + expect(result[0].workspaceName).toBe('proj'); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(tmpBase, { recursive: true, force: true }); + } + }); + // ---- workspace name resolution with spaces ---- it('resolves workspace name when path contains a folder with spaces', { timeout: 30_000 }, () => { diff --git a/src/core/parser-claude.ts b/src/core/parser-claude.ts index 73e0ab1f..74bf398c 100644 --- a/src/core/parser-claude.ts +++ b/src/core/parser-claude.ts @@ -384,11 +384,11 @@ export function findClaudeDirs(): string[] { } /** Encode a single filesystem component the way Claude Code does: - * replace whitespace with hyphens. Path separators never appear in a single + * replace whitespace and underscores with hyphens. Path separators never appear in a single * component name, and colons are not handled here because Windows component * names cannot contain them. */ function encodeComponentForMatch(name: string): string { - return name.replace(/\s/g, '-'); + return name.replace(/[\s_]/g, '-'); } /**