fix(claude): match Claude Code's project-dir slug rule#364
Merged
dpup merged 3 commits intoMay 31, 2026
Conversation
abezzub-dr
added a commit
to abezzub-dr/moat
that referenced
this pull request
May 29, 2026
WorkspaceToClaudeDir only replaced "/" with "-", but Claude Code slugifies project directories by replacing every non-alphanumeric character with "-" (verified against the claude binary v2.1.156). For any workspace path containing a "." (e.g. a username like "user.name"), "_", or a space, moat computed a different ~/.claude/projects/ directory name than the host CLI. Because moat bind-mounts that host directory into the container's ~/.claude/projects/-workspace, container sessions and host sessions landed in two divergent project dirs — silently forking the project's session history and memory store. Replace the slug logic with a faithful port of Claude Code's rule: keep letters and digits, map everything else to a single "-", with no collapsing of runs. ASCII paths are byte-for-byte identical to Claude Code's output.
d5c149b to
834fa7b
Compare
- Guard the Claude log-sync mount against an empty workspace. An empty path slugifies to "" and collapsed filepath.Join to ~/.claude/projects, which would bind-mount the host's entire projects tree (every project's session history) into the container. Extract claudeProjectsHostDir(), which returns "" to skip the mount, and unit-test it. Unreachable today (ResolveWorkspacePath always yields an absolute path) but high-impact. - Drop the dead filepath.ToSlash call: "/", "\" and ":" are all non-alphanumeric and already map to "-", so it never changed output. - Correct the UTF-16 doc note: only characters above U+FFFF (surrogate pairs) diverge from Claude Code; BMP non-ASCII maps to one "-" on both sides.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
WorkspaceToClaudeDirbuilds the~/.claude/projects/<dir>name from a workspace path by replacing only/with-. Claude Code's actual rule replaces every non-alphanumeric character with-(verified against the claude binary v2.1.156: letters/digits kept as-is, everything else → a single-, runs not collapsed).So for any workspace path containing a
.,_, or space — e.g. a macOS username likeuser.name— moat computed a different directory name than the host CLI:/Users/user.name/repos/app→-Users-user-name-repos-app/Users/user.name/repos/app→-Users-user.name-repos-appmoat bind-mounts that host directory into the container's
~/.claude/projects/-workspace(internal/run/manager.go), so container sessions and host sessions for the same repo landed in two divergent project dirs. Result: the project's Claude session history and memory store silently forked — moat (container) sessions wrote to one store, host sessions to another.Fix
Replace the slug logic with a faithful port of Claude Code's rule: keep
[A-Za-z0-9], map every other character to a single-, no collapsing of runs. For ASCII paths (all real workspace paths) the output is byte-for-byte identical to Claude Code's. The function doc notes the one non-ASCII edge case (Claude operates on UTF-16 code units; rune iteration differs only for non-ASCII path chars, which don't occur in practice).Tests
Extended
TestWorkspaceToClaudeDirwith cases derived empirically from the claude binary: dot/underscore/space → dash, existing dashes preserved, consecutive separators not collapsed, plus a full mixed-character probe string. Every new case failed against the old implementation and passes with the fix.go test -raceis green forinternal/run/...andinternal/providers/claude/....Note (out of scope here)
This stops the fork going forward but does not migrate already-forked stores: existing session history / memory under the old dot-encoded directory won't move on its own. A one-time migration to merge the old dir into the canonical one is a separate follow-up. The fix also only takes effect once the moat binary is rebuilt/reinstalled.
🤖 Generated with Claude Code