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> = {
135135 Effect.provideService(Path.Path, path),
136136 ),
137137 );
138 const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig);
138 const continuationGroupKey = yield* makeClaudeContinuationGroupKey(
139 effectiveConfig,
140 processEnv,
141 );
139142 const stampIdentity = withInstanceIdentity({
140143 instanceId,
141144 driverKind: DRIVER_KIND,
......@@ -171,7 +174,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
171174 Effect.provideService(Path.Path, path),
172175 ),
173176 });
174 const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(effectiveConfig, cwd);
177 const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(
178 effectiveConfig,
179 cwd,
180 processEnv,
181 );
175182
176183 // Start the TTL-gated refresh without delaying provider readiness. The
177184 // 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 {
1515
1616it.layer(NodeServices.layer)("ClaudeHome", (it) => {
1717 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", () =>
1919 Effect.gen(function* () {
2020 const path = yield* Path.Path;
21 const resolved = path.resolve(NodeOS.homedir());
21 const resolved = path.resolve(path.join(NodeOS.homedir(), ".claude"));
2222
2323 expect(yield* resolveClaudeHomePath({ homePath: "" })).toBe(resolved);
24 expect(yield* resolveClaudeHomePath({ homePath: "~/.claude" })).toBe(resolved);
25 expect(yield* resolveClaudeHomePath({ homePath: resolved })).toBe(resolved);
2426 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);
2532 }),
2633 );
2734
......@@ -40,6 +47,24 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => {
4047 }),
4148 );
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
4368 it("points the signed-out hint at the configured Claude home", () => {
4469 expect(claudeSignedOutMessage({ configDir: undefined, cwd: "/synthetic" })).toContain(
4570 "run `claude auth login`",
......@@ -59,16 +84,5 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => {
5984 expect(first).not.toBe(second);
6085 }),
6186 );
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 );
7387 });
7488});
apps/server/src/provider/Drivers/ClaudeHome.ts+24-4
......@@ -9,12 +9,28 @@ import { expandHomePath } from "../../pathExpansion.ts";
99
1010const 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 */
1219export const resolveClaudeHomePath = Effect.fn("resolveClaudeHomePath")(function* (
1320 config: Pick<ClaudeSettings, "homePath">,
21 environment?: NodeJS.ProcessEnv,
1422): Effect.fn.Return<string, never, Path.Path> {
1523 const path = yield* Path.Path;
1624 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"));
1834});
1935
2036export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function* (
......@@ -38,8 +54,11 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function
3854});
3955
4056export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationGroupKey")(
41 function* (config: Pick<ClaudeSettings, "homePath">): Effect.fn.Return<string, never, Path.Path> {
42 const resolvedHomePath = yield* resolveClaudeHomePath(config);
57 function* (
58 config: Pick<ClaudeSettings, "homePath">,
59 environment?: NodeJS.ProcessEnv,
60 ): Effect.fn.Return<string, never, Path.Path> {
61 const resolvedHomePath = yield* resolveClaudeHomePath(config, environment);
4362 return `claude:home:${resolvedHomePath}`;
4463 },
4564);
......@@ -48,8 +67,9 @@ export const makeClaudeCapabilitiesCacheKey = Effect.fn("makeClaudeCapabilitiesC
4867 function* (
4968 config: Pick<ClaudeSettings, "binaryPath" | "homePath">,
5069 cwd?: string,
70 environment?: NodeJS.ProcessEnv,
5171 ): Effect.fn.Return<string, never, Path.Path> {
52 const resolvedHomePath = yield* resolveClaudeHomePath(config);
72 const resolvedHomePath = yield* resolveClaudeHomePath(config, environment);
5373 return `${config.binaryPath}\0${resolvedHomePath}\0${cwd ?? ""}`;
5474 },
5575);