From 6529db3be652b971554d864fb508db9dcda6571a Mon Sep 17 00:00:00 2001 From: Patrick Ribbsaeter Date: Tue, 25 Aug 2026 05:14:40 +0200 Subject: [PATCH] fix(han): sanitize Windows project path slugs (#105) * fix(han): sanitize Windows project path slugs Handle drive letters and backslash separators, and share one pathToSlug implementation across memory and transcript search. * fix(han): match Claude Windows slug format Preserve the separate replacements for drive colons and path separators, and teach slugToPath to decode double-dash Windows prefixes. --------- Co-authored-by: Patrick --- packages/han/lib/memory/paths.ts | 6 +++--- packages/han/lib/memory/transcript-search.ts | 18 ++++++------------ packages/han/test/han-events-path.test.ts | 7 +++++++ packages/han/test/transcript-search.test.ts | 16 ++++++++++++++++ 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/han/lib/memory/paths.ts b/packages/han/lib/memory/paths.ts index dcac648..63ef6a2 100644 --- a/packages/han/lib/memory/paths.ts +++ b/packages/han/lib/memory/paths.ts @@ -182,14 +182,14 @@ export function getClaudeProjectsDir(): string { /** * Convert a filesystem path to Claude Code's project slug format - * Claude Code replaces both `/` and `.` with `-` + * Claude Code replaces path separators, dots, and drive-letter colons with `-`. * * @example * pathToSlug("/Volumes/dev/src/github.com/foo/bar") // "-Volumes-dev-src-github-com-foo-bar" + * pathToSlug("C:\\Users\\foo\\bar") // "C--Users-foo-bar" */ export function pathToSlug(fsPath: string): string { - // Replace path separators and dots with dashes, removing leading slashes - return fsPath.replace(/^\//, '-').replace(/[/.]/g, '-'); + return fsPath.replace(/[\\/:.]/g, '-'); } /** diff --git a/packages/han/lib/memory/transcript-search.ts b/packages/han/lib/memory/transcript-search.ts index b5a191b..83d22b0 100644 --- a/packages/han/lib/memory/transcript-search.ts +++ b/packages/han/lib/memory/transcript-search.ts @@ -23,7 +23,9 @@ import { initTable, searchFts, } from './indexer.ts'; -import { getGitRemote, normalizeGitRemote } from './paths.ts'; +import { getGitRemote, normalizeGitRemote, pathToSlug } from './paths.ts'; + +export { pathToSlug } from './paths.ts'; /** * Transcript message types @@ -135,14 +137,6 @@ export function getClaudeProjectsDir(): string { return join(home, '.claude', 'projects'); } -/** - * Convert a filesystem path to Claude project slug - * e.g., /Volumes/dev/src/github.com/foo -> -Volumes-dev-src-github-com-foo - */ -export function pathToSlug(path: string): string { - return path.replace(/[/.]/g, '-'); -} - /** * Convert a Claude project slug back to filesystem path * e.g., -Volumes-dev-src-github-com-foo -> /Volumes/dev/src/github.com/foo @@ -175,9 +169,9 @@ export function slugToPath(slug: string): string { else if (path.startsWith('tmp-') || path.startsWith('private-tmp-')) { path = `/${path.replace(/-/g, '/')}`; } - // C:\... (Windows) - else if (/^[A-Z]-/.test(path)) { - path = `${path[0]}:\\${path.slice(2).replace(/-/g, '\\')}`; + // C:\... (Windows); Claude replaces both the drive colon and separator. + else if (/^[A-Za-z]--/.test(path)) { + path = `${path[0]}:\\${path.slice(3).replace(/-/g, '\\')}`; } // Default: assume Unix-style path else { diff --git a/packages/han/test/han-events-path.test.ts b/packages/han/test/han-events-path.test.ts index 36cc208..1e79803 100644 --- a/packages/han/test/han-events-path.test.ts +++ b/packages/han/test/han-events-path.test.ts @@ -11,6 +11,7 @@ import { getHanEventsFilePath, getSessionFilePath, getSessionsPath, + pathToSlug, setMemoryRoot, } from '../lib/memory/paths.ts'; @@ -88,4 +89,10 @@ describe('Han Events Path Resolution', () => { expect(hanEventsPath).toContain(date); }); + + test('project slugs sanitize Windows drive letters and separators', () => { + expect(pathToSlug('C:\\Users\\user\\Projects\\example-app')).toBe( + 'C--Users-user-Projects-example-app' + ); + }); }); diff --git a/packages/han/test/transcript-search.test.ts b/packages/han/test/transcript-search.test.ts index 91bdd72..e781081 100644 --- a/packages/han/test/transcript-search.test.ts +++ b/packages/han/test/transcript-search.test.ts @@ -61,6 +61,22 @@ describe.serial('transcript search', () => { expect(pathToSlug('/github.com/org/repo')).toBe('-github-com-org-repo'); }); + test('pathToSlug converts Windows paths to a single slug', async () => { + const { pathToSlug } = await import('../lib/memory/transcript-search.ts'); + + expect(pathToSlug('C:\\Users\\user\\Projects\\example-app')).toBe( + 'C--Users-user-Projects-example-app' + ); + }); + + test('slugToPath restores Windows project slugs', async () => { + const { slugToPath } = await import('../lib/memory/transcript-search.ts'); + + expect(slugToPath('C--Users-user-Projects-example-app')).toBe( + 'C:\\Users\\user\\Projects\\example\\app' + ); + }); + test('slugToPath converts macOS Volumes paths', async () => { const { slugToPath } = await import('../lib/memory/transcript-search.ts'); -- 2.55.0.windows.3