Skip to content

[BUG] pathToSlug doesn't sanitize Windows paths — SessionEnd hook fails with mkdir ENOENT #104

Description

@mathbreder

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

  1. Use han on Windows, inside a project directory such as C:\Users\<user>\Projects\example-app.
  2. Start and end a Claude Code session with the han core plugin active.
  3. Observe the SessionEnd hook failure logged when the session ends.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions