-
-
Notifications
You must be signed in to change notification settings - Fork 19
WSL support: Cannot import Claude Code sessions from WSL filesystem #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,7 @@ import { getClaudeModelsCache, setClaudeModelsCache } from "../lib/claude-model- | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { reportError } from "../lib/error-utils"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getClaudeBinaryMetadata, getClaudeBinaryPath, getClaudeBinaryStatus, getClaudeVersion } from "../lib/claude-binary"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { captureEvent } from "../lib/posthog"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { maybeToWslPath } from "../lib/wsl-path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** SDK options for file checkpointing — enables Write/Edit/NotebookEdit revert support */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function fileCheckpointOptions(): Record<string, unknown> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -45,6 +46,12 @@ interface SessionEntry { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const sessions = new Map<string, SessionEntry>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function resolveCwd(raw?: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const candidate = raw && raw.trim() ? raw : process.cwd(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const translated = maybeToWslPath(candidate); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const translated = maybeToWslPath(candidate); | |
| let shouldTranslate = false; | |
| try { | |
| const cliInfo: unknown = getCliPath(); | |
| if (cliInfo && typeof cliInfo === "object") { | |
| const info = cliInfo as { | |
| strategy?: string; | |
| resolutionStrategy?: string; | |
| cliPath?: string; | |
| path?: string; | |
| }; | |
| const strategy = info.strategy ?? info.resolutionStrategy; | |
| const cliPath = info.cliPath ?? info.path; | |
| if (strategy === "wsl") { | |
| shouldTranslate = true; | |
| } else if (typeof cliPath === "string") { | |
| const lowerPath = cliPath.toLowerCase(); | |
| if (lowerPath.includes("wsl")) { | |
| shouldTranslate = true; | |
| } | |
| } | |
| } | |
| } catch { | |
| // If CLI resolution fails for any reason, fall back to no translation. | |
| } | |
| const translated = shouldTranslate ? maybeToWslPath(candidate) : null; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { getLikelyWslHome, maybeToWslPath, parseWslPath, getWslWindowsPrefix } from "../wsl-path"; | ||
|
|
||
| describe("wsl path helpers", () => { | ||
| it("parses UNC WSL paths into distro and unix paths", () => { | ||
| const info = parseWslPath("\\\\wsl$\\Ubuntu\\home\\user\\project"); | ||
| expect(info).toEqual({ distro: "Ubuntu", unixPath: "/home/user/project" }); | ||
|
|
||
| const localhost = parseWslPath("//wsl.localhost/Ubuntu/home/user/project"); | ||
| expect(localhost).toEqual({ distro: "Ubuntu", unixPath: "/home/user/project" }); | ||
| }); | ||
|
|
||
| it("returns the original path when not a WSL UNC path", () => { | ||
| expect(maybeToWslPath("C:\\\\Users\\\\tester")).toBe("C:\\\\Users\\\\tester"); | ||
| expect(maybeToWslPath(undefined)).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("guesses the user home directory from a WSL path", () => { | ||
| expect(getLikelyWslHome("/home/alice/code")).toBe("/home/alice"); | ||
| expect(getLikelyWslHome("/var/www")).toBe("/root"); | ||
| }); | ||
|
|
||
| it("builds the UNC prefix for a distro using wsl.localhost", () => { | ||
| expect(getWslWindowsPrefix("Ubuntu")).toBe("\\\\wsl.localhost\\Ubuntu"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,7 +8,7 @@ import { log } from "./logger"; | |||||||
| import { getCliPath } from "./sdk"; | ||||||||
|
|
||||||||
| export type ClaudeBinarySource = "auto" | "managed" | "custom"; | ||||||||
| export type ClaudeBinaryResolutionStrategy = "custom" | "env" | "known" | "path" | "sdk-fallback"; | ||||||||
| export type ClaudeBinaryResolutionStrategy = "custom" | "env" | "known" | "path" | "wsl" | "sdk-fallback"; | ||||||||
|
|
||||||||
| interface ResolveClaudeBinaryOptions { | ||||||||
| installIfMissing?: boolean; | ||||||||
|
|
@@ -62,6 +62,35 @@ function getKnownPaths(): string[] { | |||||||
| return [path.join(os.homedir(), ".local", "bin", "claude")]; | ||||||||
| } | ||||||||
|
|
||||||||
| const WSL_WRAPPER_DIR = path.join(os.tmpdir(), "harnss-claude-wsl"); | ||||||||
| const WSL_WRAPPER_NAME = "claude-wsl-wrapper.cmd"; | ||||||||
|
|
||||||||
| function getWslWrapperPath(): string { | ||||||||
| return path.join(WSL_WRAPPER_DIR, WSL_WRAPPER_NAME); | ||||||||
| } | ||||||||
|
|
||||||||
| function ensureWslWrapper(wslPath: string): string | null { | ||||||||
| const wrapperPath = getWslWrapperPath(); | ||||||||
| const script = `@echo off\r\nwsl.exe -e ${wslPath} %*\r\n`; | ||||||||
|
||||||||
| const script = `@echo off\r\nwsl.exe -e ${wslPath} %*\r\n`; | |
| const quotedWslPath = `"${wslPath.replace(/"/g, '""')}"`; | |
| const script = `@echo off\r\nwsl.exe -e ${quotedWslPath} -- %*\r\n`; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| export interface WslPathInfo { | ||
| distro: string; | ||
| unixPath: string; | ||
| } | ||
|
|
||
| const WSL_PATH_REGEX = /^\/\/wsl(?:\.localhost|\$)\/([^/]+)(\/.*)/i; | ||
|
|
||
| /** | ||
| * Detect a Windows WSL UNC path (\\wsl$\\Distro\\... or \\wsl.localhost\\Distro\\...) | ||
| * and return the distro name plus the corresponding WSL unix path. | ||
| */ | ||
| export function parseWslPath(rawPath?: string): WslPathInfo | null { | ||
| if (!rawPath) return null; | ||
| const normalized = rawPath.replace(/\\/g, "/"); | ||
| const match = normalized.match(WSL_PATH_REGEX); | ||
| if (!match) return null; | ||
|
|
||
| const distro = match[1]; | ||
| const unixPath = match[2] || "/"; | ||
| return { distro, unixPath }; | ||
| } | ||
|
|
||
| /** Best-effort guess of the WSL user's home directory from a project path. */ | ||
| export function getLikelyWslHome(unixPath: string): string { | ||
| const homeMatch = unixPath.match(/^\/home\/[^/]+/); | ||
| if (homeMatch) return homeMatch[0]; | ||
| return "/root"; | ||
|
Comment on lines
+24
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Avoid defaulting WSL home to Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| /** UNC prefix for a given WSL distro (wsl.localhost used for consistency). */ | ||
| export function getWslWindowsPrefix(distro: string): string { | ||
| return `\\\\wsl.localhost\\${distro}`; | ||
| } | ||
|
|
||
| /** Translate a WSL UNC path to its unix equivalent; leave other paths untouched. */ | ||
| export function maybeToWslPath(rawPath?: string): string | undefined { | ||
| const info = parseWslPath(rawPath); | ||
| return info ? info.unixPath : rawPath; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getLikelyWslHome()infers the WSL user home from the project path and falls back to/root. For WSL projects located outside/home/<user>(e.g./mnt/c/...), this will point session import at\\wsl.localhost\<distro>\root\.claude\..., which is usually wrong and will still yield “No sessions found”. Consider resolving the actual WSL$HOMEfor the distro viawsl.exe -d <distro> -e sh -lc 'printf %s "$HOME"'(with caching) instead of guessing from the project path.