From 567c15176df6f13c3652cafa69936c52f3c1d474 Mon Sep 17 00:00:00 2001 From: Cestercian Date: Sat, 19 Sep 2026 20:08:23 +0000 Subject: [PATCH] fix(server): empty Claude homePath shares continuation with ~/.claude Empty CLAUDE_CONFIG_DIR resolved to $HOME, so the default instance and an explicit ~/.claude got different continuation group keys even though Claude stores sessions in ~/.claude either way. Resolve empty/unset to ~/.claude (and inherited CLAUDE_CONFIG_DIR when set) so those configs can switch mid-thread. --- .../src/provider/Drivers/ClaudeDriver.ts | 11 ++++- .../src/provider/Drivers/ClaudeHome.test.ts | 40 +++++++++++++------ .../server/src/provider/Drivers/ClaudeHome.ts | 28 +++++++++++-- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts index 324284a3a4c7..b87a89dcf528 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts @@ -135,7 +135,10 @@ export const ClaudeDriver: ProviderDriver = { Effect.provideService(Path.Path, path), ), ); - const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig); + const continuationGroupKey = yield* makeClaudeContinuationGroupKey( + effectiveConfig, + processEnv, + ); const stampIdentity = withInstanceIdentity({ instanceId, driverKind: DRIVER_KIND, @@ -171,7 +174,11 @@ export const ClaudeDriver: ProviderDriver = { Effect.provideService(Path.Path, path), ), }); - const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(effectiveConfig, cwd); + const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey( + effectiveConfig, + cwd, + processEnv, + ); // Start the TTL-gated refresh without delaying provider readiness. The // next check observes a remote manifest after the background fetch lands. diff --git a/apps/server/src/provider/Drivers/ClaudeHome.test.ts b/apps/server/src/provider/Drivers/ClaudeHome.test.ts index 33b11237547e..fb1caf753f86 100644 --- a/apps/server/src/provider/Drivers/ClaudeHome.test.ts +++ b/apps/server/src/provider/Drivers/ClaudeHome.test.ts @@ -15,13 +15,20 @@ import { it.layer(NodeServices.layer)("ClaudeHome", (it) => { describe("Claude home resolution", () => { - it.effect("uses the process home when no Claude home override is configured", () => + it.effect("treats empty, ~/.claude, and the expanded default as the same Claude home", () => Effect.gen(function* () { const path = yield* Path.Path; - const resolved = path.resolve(NodeOS.homedir()); + const resolved = path.resolve(path.join(NodeOS.homedir(), ".claude")); expect(yield* resolveClaudeHomePath({ homePath: "" })).toBe(resolved); + expect(yield* resolveClaudeHomePath({ homePath: "~/.claude" })).toBe(resolved); + expect(yield* resolveClaudeHomePath({ homePath: resolved })).toBe(resolved); expect(yield* makeClaudeEnvironment({ homePath: "" })).toBe(process.env); + + const key = `claude:home:${resolved}`; + expect(yield* makeClaudeContinuationGroupKey({ homePath: "" })).toBe(key); + expect(yield* makeClaudeContinuationGroupKey({ homePath: "~/.claude" })).toBe(key); + expect(yield* makeClaudeContinuationGroupKey({ homePath: resolved })).toBe(key); }), ); @@ -40,6 +47,24 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => { }), ); + it.effect("uses inherited CLAUDE_CONFIG_DIR when homePath is empty", () => + Effect.gen(function* () { + const path = yield* Path.Path; + const inherited = path.resolve("/tmp/claude-inherited"); + const environment = { CLAUDE_CONFIG_DIR: inherited }; + + expect(yield* resolveClaudeHomePath({ homePath: "" }, environment)).toBe(inherited); + expect(yield* makeClaudeContinuationGroupKey({ homePath: "" }, environment)).toBe( + `claude:home:${inherited}`, + ); + + const explicit = path.resolve(NodeOS.homedir(), ".claude-work"); + expect(yield* resolveClaudeHomePath({ homePath: "~/.claude-work" }, environment)).toBe( + explicit, + ); + }), + ); + it("points the signed-out hint at the configured Claude home", () => { expect(claudeSignedOutMessage({ configDir: undefined, cwd: "/synthetic" })).toContain( "run `claude auth login`", @@ -59,16 +84,5 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => { expect(first).not.toBe(second); }), ); - - it.effect("keeps continuation compatible across instances with the same Claude HOME", () => - Effect.gen(function* () { - const path = yield* Path.Path; - const resolved = path.resolve(NodeOS.homedir()); - - expect(yield* makeClaudeContinuationGroupKey({ homePath: "" })).toBe( - `claude:home:${resolved}`, - ); - }), - ); }); }); diff --git a/apps/server/src/provider/Drivers/ClaudeHome.ts b/apps/server/src/provider/Drivers/ClaudeHome.ts index bbd005a1e00b..70699ca669e8 100644 --- a/apps/server/src/provider/Drivers/ClaudeHome.ts +++ b/apps/server/src/provider/Drivers/ClaudeHome.ts @@ -9,12 +9,28 @@ import { expandHomePath } from "../../pathExpansion.ts"; const quotePath = Schema.encodeSync(Schema.fromJsonString(Schema.String)); +/** + * Resolve the Claude config directory the CLI would use: the instance's + * `homePath` (exported as `CLAUDE_CONFIG_DIR`), then an inherited + * `CLAUDE_CONFIG_DIR`, then Claude's default `~/.claude`. Empty must not + * fall back to bare `$HOME` — that leftover from the old HOME override + * produced a different continuation group than an explicit `~/.claude`. + */ export const resolveClaudeHomePath = Effect.fn("resolveClaudeHomePath")(function* ( config: Pick, + environment?: NodeJS.ProcessEnv, ): Effect.fn.Return { const path = yield* Path.Path; const homePath = config.homePath.trim(); - return path.resolve(homePath.length > 0 ? expandHomePath(homePath) : NodeOS.homedir()); + if (homePath.length > 0) { + return path.resolve(expandHomePath(homePath)); + } + // Inherited env vars are not shell-expanded, so a literal `~` stays literal. + const inherited = environment?.CLAUDE_CONFIG_DIR?.trim() ?? ""; + if (inherited.length > 0) { + return path.resolve(inherited); + } + return path.resolve(path.join(NodeOS.homedir(), ".claude")); }); export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function* ( @@ -38,8 +54,11 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function }); export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationGroupKey")( - function* (config: Pick): Effect.fn.Return { - const resolvedHomePath = yield* resolveClaudeHomePath(config); + function* ( + config: Pick, + environment?: NodeJS.ProcessEnv, + ): Effect.fn.Return { + const resolvedHomePath = yield* resolveClaudeHomePath(config, environment); return `claude:home:${resolvedHomePath}`; }, ); @@ -48,8 +67,9 @@ export const makeClaudeCapabilitiesCacheKey = Effect.fn("makeClaudeCapabilitiesC function* ( config: Pick, cwd?: string, + environment?: NodeJS.ProcessEnv, ): Effect.fn.Return { - const resolvedHomePath = yield* resolveClaudeHomePath(config); + const resolvedHomePath = yield* resolveClaudeHomePath(config, environment); return `${config.binaryPath}\0${resolvedHomePath}\0${cwd ?? ""}`; }, );