authorgravatar for yashafaid@gmail.comCestercian <yashafaid@gmail.com> 2026-09-20 03:39:05+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-19 15:09:05-07:00
log63ff33756c47f127391962458d8388a647e8ed44
tree549a3f186f5f5059dc06bde9ee3537669db5fca7
parente36725682bd2935f1bcfdf81660cb569b5f3df29
signature Signed by PGP key B5690EEEBB952194

fix(server): empty Claude homePath shares continuation with ~/.claude (#12624)


3 files changed, 60 insertions(+), 19 deletions(-)

apps/server/src/provider/Drivers/ClaudeDriver.ts+9-2
...@@ -135,7 +135,10 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {...@@ -135,7 +135,10 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
135 Effect.provideService(Path.Path, path),135 Effect.provideService(Path.Path, path),
136 ),136 ),
137 );137 );
138 const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig);138 const continuationGroupKey = yield* makeClaudeContinuationGroupKey(
139 effectiveConfig,
140 processEnv,
141 );
139 const stampIdentity = withInstanceIdentity({142 const stampIdentity = withInstanceIdentity({
140 instanceId,143 instanceId,
141 driverKind: DRIVER_KIND,144 driverKind: DRIVER_KIND,
...@@ -171,7 +174,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {...@@ -171,7 +174,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
171 Effect.provideService(Path.Path, path),174 Effect.provideService(Path.Path, path),
172 ),175 ),
173 });176 });
174 const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(effectiveConfig, cwd);177 const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(
178 effectiveConfig,
179 cwd,
180 processEnv,
181 );
175182
176 // Start the TTL-gated refresh without delaying provider readiness. The183 // Start the TTL-gated refresh without delaying provider readiness. The
177 // next check observes a remote manifest after the background fetch lands.184 // next check observes a remote manifest after the background fetch lands.
apps/server/src/provider/Drivers/ClaudeHome.test.ts+27-13
...@@ -15,13 +15,20 @@ import {...@@ -15,13 +15,20 @@ import {
1515
16it.layer(NodeServices.layer)("ClaudeHome", (it) => {16it.layer(NodeServices.layer)("ClaudeHome", (it) => {
17 describe("Claude home resolution", () => {17 describe("Claude home resolution", () => {
18 it.effect("uses the process home when no Claude home override is configured", () =>18 it.effect("treats empty, ~/.claude, and the expanded default as the same Claude home", () =>
19 Effect.gen(function* () {19 Effect.gen(function* () {
20 const path = yield* Path.Path;20 const path = yield* Path.Path;
21 const resolved = path.resolve(NodeOS.homedir());21 const resolved = path.resolve(path.join(NodeOS.homedir(), ".claude"));
2222
23 expect(yield* resolveClaudeHomePath({ homePath: "" })).toBe(resolved);23 expect(yield* resolveClaudeHomePath({ homePath: "" })).toBe(resolved);
24 expect(yield* resolveClaudeHomePath({ homePath: "~/.claude" })).toBe(resolved);
25 expect(yield* resolveClaudeHomePath({ homePath: resolved })).toBe(resolved);
24 expect(yield* makeClaudeEnvironment({ homePath: "" })).toBe(process.env);26 expect(yield* makeClaudeEnvironment({ homePath: "" })).toBe(process.env);
27
28 const key = `claude:home:${resolved}`;
29 expect(yield* makeClaudeContinuationGroupKey({ homePath: "" })).toBe(key);
30 expect(yield* makeClaudeContinuationGroupKey({ homePath: "~/.claude" })).toBe(key);
31 expect(yield* makeClaudeContinuationGroupKey({ homePath: resolved })).toBe(key);
25 }),32 }),
26 );33 );
2734
...@@ -40,6 +47,24 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => {...@@ -40,6 +47,24 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => {
40 }),47 }),
41 );48 );
4249
50 it.effect("uses inherited CLAUDE_CONFIG_DIR when homePath is empty", () =>
51 Effect.gen(function* () {
52 const path = yield* Path.Path;
53 const inherited = path.resolve("/tmp/claude-inherited");
54 const environment = { CLAUDE_CONFIG_DIR: inherited };
55
56 expect(yield* resolveClaudeHomePath({ homePath: "" }, environment)).toBe(inherited);
57 expect(yield* makeClaudeContinuationGroupKey({ homePath: "" }, environment)).toBe(
58 `claude:home:${inherited}`,
59 );
60
61 const explicit = path.resolve(NodeOS.homedir(), ".claude-work");
62 expect(yield* resolveClaudeHomePath({ homePath: "~/.claude-work" }, environment)).toBe(
63 explicit,
64 );
65 }),
66 );
67
43 it("points the signed-out hint at the configured Claude home", () => {68 it("points the signed-out hint at the configured Claude home", () => {
44 expect(claudeSignedOutMessage({ configDir: undefined, cwd: "/synthetic" })).toContain(69 expect(claudeSignedOutMessage({ configDir: undefined, cwd: "/synthetic" })).toContain(
45 "run `claude auth login`",70 "run `claude auth login`",
...@@ -59,16 +84,5 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => {...@@ -59,16 +84,5 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => {
59 expect(first).not.toBe(second);84 expect(first).not.toBe(second);
60 }),85 }),
61 );86 );
62
63 it.effect("keeps continuation compatible across instances with the same Claude HOME", () =>
64 Effect.gen(function* () {
65 const path = yield* Path.Path;
66 const resolved = path.resolve(NodeOS.homedir());
67
68 expect(yield* makeClaudeContinuationGroupKey({ homePath: "" })).toBe(
69 `claude:home:${resolved}`,
70 );
71 }),
72 );
73 });87 });
74});88});
apps/server/src/provider/Drivers/ClaudeHome.ts+24-4
...@@ -9,12 +9,28 @@ import { expandHomePath } from "../../pathExpansion.ts";...@@ -9,12 +9,28 @@ import { expandHomePath } from "../../pathExpansion.ts";
99
10const quotePath = Schema.encodeSync(Schema.fromJsonString(Schema.String));10const quotePath = Schema.encodeSync(Schema.fromJsonString(Schema.String));
1111
12/**
13 * Resolve the Claude config directory the CLI would use: the instance's
14 * `homePath` (exported as `CLAUDE_CONFIG_DIR`), then an inherited
15 * `CLAUDE_CONFIG_DIR`, then Claude's default `~/.claude`. Empty must not
16 * fall back to bare `$HOME` — that leftover from the old HOME override
17 * produced a different continuation group than an explicit `~/.claude`.
18 */
12export const resolveClaudeHomePath = Effect.fn("resolveClaudeHomePath")(function* (19export const resolveClaudeHomePath = Effect.fn("resolveClaudeHomePath")(function* (
13 config: Pick<ClaudeSettings, "homePath">,20 config: Pick<ClaudeSettings, "homePath">,
21 environment?: NodeJS.ProcessEnv,
14): Effect.fn.Return<string, never, Path.Path> {22): Effect.fn.Return<string, never, Path.Path> {
15 const path = yield* Path.Path;23 const path = yield* Path.Path;
16 const homePath = config.homePath.trim();24 const homePath = config.homePath.trim();
17 return path.resolve(homePath.length > 0 ? expandHomePath(homePath) : NodeOS.homedir());25 if (homePath.length > 0) {
26 return path.resolve(expandHomePath(homePath));
27 }
28 // Inherited env vars are not shell-expanded, so a literal `~` stays literal.
29 const inherited = environment?.CLAUDE_CONFIG_DIR?.trim() ?? "";
30 if (inherited.length > 0) {
31 return path.resolve(inherited);
32 }
33 return path.resolve(path.join(NodeOS.homedir(), ".claude"));
18});34});
1935
20export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function* (36export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function* (
...@@ -38,8 +54,11 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function...@@ -38,8 +54,11 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function
38});54});
3955
40export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationGroupKey")(56export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationGroupKey")(
41 function* (config: Pick<ClaudeSettings, "homePath">): Effect.fn.Return<string, never, Path.Path> {57 function* (
42 const resolvedHomePath = yield* resolveClaudeHomePath(config);58 config: Pick<ClaudeSettings, "homePath">,
59 environment?: NodeJS.ProcessEnv,
60 ): Effect.fn.Return<string, never, Path.Path> {
61 const resolvedHomePath = yield* resolveClaudeHomePath(config, environment);
43 return `claude:home:${resolvedHomePath}`;62 return `claude:home:${resolvedHomePath}`;
44 },63 },
45);64);
...@@ -48,8 +67,9 @@ export const makeClaudeCapabilitiesCacheKey = Effect.fn("makeClaudeCapabilitiesC...@@ -48,8 +67,9 @@ export const makeClaudeCapabilitiesCacheKey = Effect.fn("makeClaudeCapabilitiesC
48 function* (67 function* (
49 config: Pick<ClaudeSettings, "binaryPath" | "homePath">,68 config: Pick<ClaudeSettings, "binaryPath" | "homePath">,
50 cwd?: string,69 cwd?: string,
70 environment?: NodeJS.ProcessEnv,
51 ): Effect.fn.Return<string, never, Path.Path> {71 ): Effect.fn.Return<string, never, Path.Path> {
52 const resolvedHomePath = yield* resolveClaudeHomePath(config);72 const resolvedHomePath = yield* resolveClaudeHomePath(config, environment);
53 return `${config.binaryPath}\0${resolvedHomePath}\0${cwd ?? ""}`;73 return `${config.binaryPath}\0${resolvedHomePath}\0${cwd ?? ""}`;
54 },74 },
55);75);