Describe the Bug
pathToSlug in packages/han/lib/memory/paths.ts (and the duplicated implementation in packages/han/lib/memory/transcript-search.ts) only sanitizes Unix-style path separators:
export function pathToSlug(fsPath: string): string {
// Replace path separators and dots with dashes, removing leading slashes
return fsPath.replace(/^\//, '-').replace(/[/.]/g, '-');
}
On Windows, process.cwd() returns paths like C:\Users\<user>\Projects\example-app, which contain no / and no leading slash — so both regexes are no-ops. The function returns the path essentially unchanged, keeping the drive-letter colon (C:) and backslashes (\). That value is then passed into join(getClaudeProjectsDir(), slug) and mkdirSync(...), which Windows rejects because : is only valid immediately after a drive letter and unescaped \ is treated as a path separator.
This breaks the SessionEnd hook (han hook dispatch SessionEnd) on every session close on Windows, and likely any other path that calls getClaudeProjectPath / getHanEventsFilePath (two-argument form) or transcript-search.ts's pathToSlug — e.g. transcript-filter hook, memory indexing.
To Reproduce
- Use han on Windows, inside a project directory such as
C:\Users\<user>\Projects\example-app.
- Start and end a Claude Code session with the
han core plugin active.
- Observe the
SessionEnd hook failure logged when the session ends.
- Alternatively, reproduce directly in code:
import { pathToSlug } from './packages/han/lib/memory/paths.ts';
pathToSlug('C:\\Users\\user\\Projects\\example-app');
Expected Behavior
pathToSlug should sanitize Windows-style separators too, producing a single valid directory-name component, e.g. C--Users-user-Projects-example-app — consistent with Claude Code's own project-slug convention, which already replaces colons and backslashes.
Actual Behavior
pathToSlug('C:\\Users\\user\\Projects\\example-app') returns the path unchanged ('C:\\Users\\user\\Projects\\example-app'). When used to build a target directory via join(getClaudeProjectsDir(), slug), the resulting string contains a second drive-letter-like segment (C:) partway through, which mkdirSync cannot create on Windows, producing an ENOENT error.
Environment
- OS: Windows 11
- Node version: n/a (han runs as a compiled Bun binary,
han-windows-x64)
- han version: 3.19.2 (latest tag at time of writing)
- Installation method: Claude Code plugin (
han core plugin)
Logs/Output
SessionEnd hook [han hook dispatch SessionEnd] failed: 1
...
ENOENT: no such file or directory, mkdir 'C:\Users\<user>\.claude\projects\C:\Users\<user>\Projects\example-app'
path: "C:\\Users\\<user>\\.claude\\projects\\C:\\Users\\<user>\\Projects\\example-app"
syscall: "mkdir"
errno: -2
code: "ENOENT"
Additional Context
- Impact appears limited to the affected hook/feature failing (non-fatal, 5s timeout) rather than crashing the session, but it silently breaks per-project memory/event persistence for every Windows user.
- Existing tests only cover Unix-style paths (
test/transcript-search.test.ts: pathToSlug converts paths correctly, pathToSlug handles dots and slashes) — no Windows-path case exists.
- Suggested fix — extend
pathToSlug to also strip/replace \ and the drive-letter :, e.g.:
export function pathToSlug(fsPath: string): string {
return fsPath
.replace(/^\//, '-')
.replace(/^([A-Za-z]):/, '$1') // drop drive-letter colon
.replace(/[/.\\:]/g, '-');
}
The exact output format should match whatever slug convention getClaudeProjectsDir() / Claude Code itself expects, so han-generated directories line up with the ones Claude Code creates. A Windows-path test case should be added alongside the fix to prevent regression.
Describe the Bug
pathToSluginpackages/han/lib/memory/paths.ts(and the duplicated implementation inpackages/han/lib/memory/transcript-search.ts) only sanitizes Unix-style path separators:On Windows,
process.cwd()returns paths likeC:\Users\<user>\Projects\example-app, which contain no/and no leading slash — so both regexes are no-ops. The function returns the path essentially unchanged, keeping the drive-letter colon (C:) and backslashes (\). That value is then passed intojoin(getClaudeProjectsDir(), slug)andmkdirSync(...), which Windows rejects because:is only valid immediately after a drive letter and unescaped\is treated as a path separator.This breaks the
SessionEndhook (han hook dispatch SessionEnd) on every session close on Windows, and likely any other path that callsgetClaudeProjectPath/getHanEventsFilePath(two-argument form) ortranscript-search.ts'spathToSlug— e.g. transcript-filter hook, memory indexing.To Reproduce
C:\Users\<user>\Projects\example-app.hancore plugin active.SessionEndhook failure logged when the session ends.Expected Behavior
pathToSlugshould sanitize Windows-style separators too, producing a single valid directory-name component, e.g.C--Users-user-Projects-example-app— consistent with Claude Code's own project-slug convention, which already replaces colons and backslashes.Actual Behavior
pathToSlug('C:\\Users\\user\\Projects\\example-app')returns the path unchanged ('C:\\Users\\user\\Projects\\example-app'). When used to build a target directory viajoin(getClaudeProjectsDir(), slug), the resulting string contains a second drive-letter-like segment (C:) partway through, whichmkdirSynccannot create on Windows, producing anENOENTerror.Environment
han-windows-x64)hancore plugin)Logs/Output
Additional Context
test/transcript-search.test.ts:pathToSlug converts paths correctly,pathToSlug handles dots and slashes) — no Windows-path case exists.pathToSlugto also strip/replace\and the drive-letter:, e.g.:getClaudeProjectsDir()/ Claude Code itself expects, so han-generated directories line up with the ones Claude Code creates. A Windows-path test case should be added alongside the fix to prevent regression.