Skip to content

Fix silent save failures on Windows + Git Bash (path slug mismatch)#44

Merged
fdaviddpt merged 2 commits intoDigital-Process-Tools:mainfrom
kanelavish-a11y:fix/windows-path-slug
May 2, 2026
Merged

Fix silent save failures on Windows + Git Bash (path slug mismatch)#44
fdaviddpt merged 2 commits intoDigital-Process-Tools:mainfrom
kanelavish-a11y:fix/windows-path-slug

Conversation

@kanelavish-a11y
Copy link
Copy Markdown
Contributor

Problem

On Windows with Git Bash (the default shell Claude Code uses for hooks on Windows), the plugin silently never saves anything. .remember/ accumulates logs/memory-YYYY-MM-DD.log showing [hook] post-tool lines, but no now.md, today-*.md, or tmp/last-save.json is ever created. There are no error messages — the hook just exits cleanly.

Root cause

Claude Code stores session JSONL files at ~/.claude/projects/<slug>/, where <slug> is the project root path with non-alphanumerics replaced by -. On Windows, Claude Code computes the slug from the Win32-native path:

C:\Users\dev\project   →   C--Users-dev-project

The plugin computes the slug from CLAUDE_PROJECT_DIR, which Git Bash/MSYS exposes as a POSIX-style path:

/c/Users/dev/project   →   -c-Users-dev-project   ← does not match

post-tool-hook.sh:46 does:

LATEST_JSONL=$(ls -t "$SESSION_DIR"/*.jsonl 2>/dev/null | head -1)
[ -n "$LATEST_JSONL" ] || exit 0

— so on Windows it always exits early. save-session.sh is never invoked, no Haiku call, no now.md. The existing test_session_dir_windows_backslash covers D:\Users\dev\project but not the /d/Users/dev/project form that Git Bash actually produces.

Fix

Normalize CLAUDE_PROJECT_DIR to its Win32 form inside resolve-paths.sh so all downstream slug computations (3 shell sites + Python _session_dir) match Claude Code's storage path.

case "\$OSTYPE" in
    msys|cygwin)
        if [[ "\$PROJECT_DIR" =~ ^/([a-zA-Z])/(.*)\$ ]]; then
            _drive="\${BASH_REMATCH[1]^^}"
            _rest="\${BASH_REMATCH[2]//\//\\}"
            PROJECT_DIR="\${_drive}:\\\${_rest}"
        fi
        ;;
esac

Pure-bash regex (no cygpath dependency for CI portability); upper-cases the drive letter and converts forward slashes to backslashes before the existing path-existence validation. On Linux/macOS bash \$OSTYPE is linux-gnu or darwin*, so the case never matches and PROJECT_DIR is left untouched.

Verification

End-to-end verified on Windows 11 + Git Bash with the v0.5.0 cached install:

$ bash <plugin>/scripts/save-session.sh --force
[hook] save-session: PROJECT_DIR=C:\Users\home\Desktop\power-dev ...
[force] bypassing cooldown + min msgs
[extract] session 5f41ebda-...
[extract] 62 exchanges (7 human)
[haiku] calling (branch: claude/...)
[tokens] tokens: 9+0cache→1166out (\$0.058894)
[write] appended: ## 06:06 | claude/...
[write] position → 279
[ndc] now.md → today-2026-05-02.md

.remember/now.md, today-2026-05-02.md, and tmp/last-save.json all created as expected.

Tests

Four new cases under TestWindowsCompatIssue11:

  • test_resolve_paths_normalizes_msys_posix_path/c/Users/dev/projectC:\Users\dev\project, including drive-letter casing and paths with spaces
  • test_resolve_paths_leaves_unix_paths_unchanged — Win32 paths, Linux home, macOS home all pass through unchanged
  • test_normalized_msys_path_yields_windows_slug — end-to-end: post-normalization slug equals the slug Claude Code uses to store sessions
  • test_resolve_paths_has_msys_normalization_block — structural guard against the block being removed

The transformation is exercised via a temp script file (not bash -c) so bash parses backslashes the same way it does in the production script.

Notes

  • \$OSTYPE is msys on Git Bash for Windows and MSYS2, cygwin on Cygwin. Both produce /c/... paths.
  • cygpath -w was an alternative implementation — chose pure-bash regex for portability/CI test simplicity (Linux CI doesn't ship cygpath).
  • Existing test_session_dir_windows_backslash and test_session_dir_windows_colon cover Win32-style input but never the POSIX-style input that Git Bash actually delivers, which is why the bug went undetected.

On Windows with Git Bash (the default shell Claude Code uses for hooks
on Windows), the plugin's PostToolUse hook fires but never saves anything.
.remember/ accumulates only logs/ — no now.md, today-*.md, or
tmp/last-save.json — and no errors are printed.

Root cause: Claude Code stores sessions under a slug computed from the
Win32 path (e.g. C:\Users\dev\project → C--Users-dev-project), but Git
Bash exposes $CLAUDE_PROJECT_DIR as a POSIX-style path
(/c/Users/dev/project → -c-Users-dev-project). The two slugs never match,
so post-tool-hook.sh exits at the LATEST_JSONL emptiness check and
save-session.sh is never invoked.

Fix: in scripts/resolve-paths.sh, detect Git Bash / MSYS / Cygwin via
$OSTYPE and convert /c/Users/... → C:\Users\... before validation. All
downstream slug computations (3 shell sites + Python _session_dir) then
align with Claude Code's storage path.

Pure-bash regex (no cygpath dependency for CI portability). Linux/macOS
shells where $OSTYPE is "linux-gnu" / "darwin*" are unaffected.

Tests: three new cases under TestWindowsCompatIssue11
- test_resolve_paths_normalizes_msys_posix_path: /c/Users/... → C:\Users\...
- test_resolve_paths_leaves_unix_paths_unchanged: Win32 + Linux/macOS pass through
- test_normalized_msys_path_yields_windows_slug: end-to-end slug match
- test_resolve_paths_has_msys_normalization_block: structural guard

Verified end-to-end on Windows 11 + Git Bash with v0.5.0 cached install:
the plugin now writes now.md and runs NDC compression as designed.
@fdaviddpt
Copy link
Copy Markdown
Contributor

Thanks @kanelavish-a11y — solid diagnosis, the POSIX form was a real gap left by #29. Fix and tests look good.

One blocker before merge: ${BASH_REMATCH[1]^^} requires bash 4+. macOS ships bash 3.2 by default, so:

  • test_resolve_paths_normalizes_msys_posix_path fails on macOS dev (drive letter stays lowercase: c:\\Users\\... instead of C:\\Users\\...).
  • Production block is gated on OSTYPE=msys|cygwin so macOS users aren't actually affected at runtime — but the failing test breaks anyone running the suite locally on macOS.

Could you swap ${BASH_REMATCH[1]^^} for a bash-3.2-compatible upper-case? Either:

_drive=$(printf '%s' "${BASH_REMATCH[1]}" | tr '[:lower:]' '[:upper:]')

or pure-bash:

_drive="${BASH_REMATCH[1]}"
case "$_drive" in
    [a-z]) _drive=$(printf '\\%o' "'$_drive" | xargs printf '%b' | tr '[:lower:]' '[:upper:]') ;;
esac

tr version is simpler and just as portable. Once that's in, happy to merge.

macOS bash 3.2 doesn't support ${var^^}. The OSTYPE-gated production
block was harmless on macOS at runtime (block skipped), but the test
forces OSTYPE=msys and reaches the unsupported syntax, breaking the
suite on any macOS dev machine using system bash.

Swap to tr '[:lower:]' '[:upper:]' — POSIX, works on bash 3.2+.
Apply same swap to the test's inlined helper.

Co-Authored-By: Max <noreply>
@fdaviddpt fdaviddpt merged commit 6884302 into Digital-Process-Tools:main May 2, 2026
fdaviddpt pushed a commit that referenced this pull request May 2, 2026
Two Windows portability fixes since v0.7.0:
- #39 detach SessionStart hook children (libuv assertion)
- #44 normalize Git Bash POSIX paths to Win32 (silent save failures)

Also: gitignore .DS_Store, remove tracked .DS_Store and stale
.github-org-profile/ (belongs in the org profile repo, not here).

Co-Authored-By: Max <noreply>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants