Skip to content

Commit 7c74e0a

Browse files
authored
fix(claude): resolve Desktop 3P paths by target platform
1 parent ff83185 commit 7c74e0a

3 files changed

Lines changed: 21 additions & 17 deletions

File tree

src/claude/desktop-3p-paths.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { homedir } from "node:os";
2-
import { join } from "node:path";
2+
import { posix, win32 } from "node:path";
33

44
/**
55
* Claude Desktop's own configLibrary location, ported from the shipped app bundle.
@@ -27,6 +27,10 @@ import { join } from "node:path";
2727
const SUFFIX = "-3p";
2828
const APP_DIR = `Claude${SUFFIX}`;
2929

30+
function joinForPlatform(platform: NodeJS.Platform, ...parts: string[]): string {
31+
return platform === "win32" ? win32.join(...parts) : posix.join(...parts);
32+
}
33+
3034
export interface DesktopPathInputs {
3135
env: Record<string, string | undefined>;
3236
platform: NodeJS.Platform;
@@ -38,11 +42,11 @@ export function resolveElectronUserData(inputs: DesktopPathInputs): string {
3842
const { env, platform, home } = inputs;
3943
if (platform === "win32") {
4044
const appData = env.APPDATA?.trim();
41-
return appData ? join(appData, "Claude") : join(home, "AppData", "Roaming", "Claude");
45+
return appData ? win32.join(appData, "Claude") : win32.join(home, "AppData", "Roaming", "Claude");
4246
}
43-
if (platform === "darwin") return join(home, "Library", "Application Support", "Claude");
47+
if (platform === "darwin") return posix.join(home, "Library", "Application Support", "Claude");
4448
const xdg = env.XDG_CONFIG_HOME?.trim();
45-
return xdg ? join(xdg, "Claude") : join(home, ".config", "Claude");
49+
return xdg ? posix.join(xdg, "Claude") : posix.join(home, ".config", "Claude");
4650
}
4751

4852
/** Desktop's userData root, mirroring `GE()` branch for branch. */
@@ -54,7 +58,7 @@ export function resolveUserDataDir(inputs: DesktopPathInputs): string {
5458

5559
if (inputs.platform === "win32") {
5660
const localAppData = inputs.env.LOCALAPPDATA?.trim();
57-
if (localAppData) return join(localAppData, APP_DIR);
61+
if (localAppData) return win32.join(localAppData, APP_DIR);
5862
}
5963

6064
const root = resolveElectronUserData(inputs);
@@ -71,7 +75,7 @@ export function resolveUserDataDir(inputs: DesktopPathInputs): string {
7175
export function resolveConfigLibraryDir(inputs: DesktopPathInputs): string {
7276
const override = inputs.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR?.trim();
7377
if (override) return override;
74-
return join(resolveUserDataDir(inputs), "configLibrary");
78+
return joinForPlatform(inputs.platform, resolveUserDataDir(inputs), "configLibrary");
7579
}
7680

7781
/** Runtime wrapper. Keep it thin: all branching lives in the pure functions above. */

tests/claude-desktop-config-path.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { expect, test, describe } from "bun:test";
22
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
33
import { homedir, tmpdir } from "node:os";
4-
import { join } from "node:path";
4+
import { join, posix, win32 } from "node:path";
55
import { startServer } from "../src/server";
66
import {
77
claudeDesktopConfigLibraryDir,
@@ -30,21 +30,21 @@ describe("Claude Desktop configLibrary resolution", () => {
3030
platform: "darwin",
3131
home: HOME,
3232
});
33-
expect(dir).toBe(join("/custom/user-data", "configLibrary"));
33+
expect(dir).toBe(posix.join("/custom/user-data", "configLibrary"));
3434
expect(dir).not.toContain("-3p");
3535
});
3636

3737
test("the macOS default stays Claude-3p (regression guard for existing users)", () => {
3838
const dir = resolveConfigLibraryDir({ env: {}, platform: "darwin", home: HOME });
39-
expect(dir).toBe(join(HOME, "Library", "Application Support", "Claude-3p", "configLibrary"));
39+
expect(dir).toBe(posix.join(HOME, "Library", "Application Support", "Claude-3p", "configLibrary"));
4040
});
4141

4242
test("win32 with LOCALAPPDATA resolves under LOCALAPPDATA, not the macOS tree", () => {
4343
const env = { LOCALAPPDATA: "C:\\Users\\tester\\AppData\\Local" };
4444
const win = resolveConfigLibraryDir({ env, platform: "win32", home: HOME });
4545
const mac = resolveConfigLibraryDir({ env, platform: "darwin", home: HOME });
4646

47-
expect(win).toBe(join("C:\\Users\\tester\\AppData\\Local", "Claude-3p", "configLibrary"));
47+
expect(win).toBe(win32.join("C:\\Users\\tester\\AppData\\Local", "Claude-3p", "configLibrary"));
4848
// The defect was returning the macOS path on Windows: prove the branch diverges.
4949
expect(win).not.toBe(mac);
5050
expect(win).not.toContain("Application Support");
@@ -56,14 +56,14 @@ describe("Claude Desktop configLibrary resolution", () => {
5656
platform: "win32",
5757
home: HOME,
5858
});
59-
expect(dir).toBe(`${join("C:\\Users\\tester\\AppData\\Roaming", "Claude")}-3p`);
59+
expect(dir).toBe(`${win32.join("C:\\Users\\tester\\AppData\\Roaming", "Claude")}-3p`);
6060
});
6161

6262
test("linux uses XDG_CONFIG_HOME when set, otherwise ~/.config", () => {
6363
expect(resolveElectronUserData({ env: { XDG_CONFIG_HOME: "/xdg" }, platform: "linux", home: HOME }))
64-
.toBe(join("/xdg", "Claude"));
64+
.toBe(posix.join("/xdg", "Claude"));
6565
expect(resolveConfigLibraryDir({ env: {}, platform: "linux", home: HOME }))
66-
.toBe(join(HOME, ".config", "Claude-3p", "configLibrary"));
66+
.toBe(posix.join(HOME, ".config", "Claude-3p", "configLibrary"));
6767
});
6868

6969
test("a userData root already ending in -3p is not double-suffixed", () => {

tests/desktop-3p.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, spyOn, test } from "bun:test";
22
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
33
import { tmpdir } from "node:os";
4-
import { join } from "node:path";
4+
import { join, posix, win32 } from "node:path";
55
import {
66
atomicReplaceDesktopConfig,
77
buildDesktop3pRegistry,
@@ -32,19 +32,19 @@ describe("Claude Desktop 3P models", () => {
3232
env: { CLAUDE_USER_DATA_DIR: "/profiles/claude" },
3333
platform: "darwin",
3434
homeDir: "/Users/test",
35-
})).toBe("/profiles/claude/configLibrary");
35+
})).toBe(posix.join("/profiles/claude", "configLibrary"));
3636
expect(resolveDesktop3pConfigLibraryPath({
3737
env: {},
3838
platform: "darwin",
3939
homeDir: "/Users/test",
4040
})).toBe("/Users/test/Library/Application Support/Claude-3p/configLibrary");
4141
// Windows reads LOCALAPPDATA first; APPDATA is only the Electron userData fallback.
42-
// Asserted with `join` because the separator follows the HOST, not the target platform.
42+
// Asserted with `win32.join` because the separator follows the target platform, not the host.
4343
expect(resolveDesktop3pConfigLibraryPath({
4444
env: { LOCALAPPDATA: "C:\\Users\\test\\AppData\\Local" },
4545
platform: "win32",
4646
homeDir: "C:\\Users\\test",
47-
})).toBe(join("C:\\Users\\test\\AppData\\Local", "Claude-3p", "configLibrary"));
47+
})).toBe(win32.join("C:\\Users\\test\\AppData\\Local", "Claude-3p", "configLibrary"));
4848
expect(resolveDesktop3pConfigLibraryPath({
4949
env: { XDG_CONFIG_HOME: "/xdg/config" },
5050
platform: "linux",

0 commit comments

Comments
 (0)