Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
Effect.provideService(Path.Path, path),
),
);
const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig);
const continuationGroupKey = yield* makeClaudeContinuationGroupKey(
effectiveConfig,
processEnv,
);
const stampIdentity = withInstanceIdentity({
instanceId,
driverKind: DRIVER_KIND,
Expand Down Expand Up @@ -171,7 +174,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
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.
Expand Down
40 changes: 27 additions & 13 deletions apps/server/src/provider/Drivers/ClaudeHome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}),
);

Expand All @@ -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`",
Expand All @@ -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}`,
);
}),
);
});
});
28 changes: 24 additions & 4 deletions apps/server/src/provider/Drivers/ClaudeHome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClaudeSettings, "homePath">,
environment?: NodeJS.ProcessEnv,
): Effect.fn.Return<string, never, Path.Path> {
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* (
Expand All @@ -38,8 +54,11 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function
});

export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationGroupKey")(
function* (config: Pick<ClaudeSettings, "homePath">): Effect.fn.Return<string, never, Path.Path> {
const resolvedHomePath = yield* resolveClaudeHomePath(config);
function* (
config: Pick<ClaudeSettings, "homePath">,
environment?: NodeJS.ProcessEnv,
): Effect.fn.Return<string, never, Path.Path> {
const resolvedHomePath = yield* resolveClaudeHomePath(config, environment);
return `claude:home:${resolvedHomePath}`;
},
);
Expand All @@ -48,8 +67,9 @@ export const makeClaudeCapabilitiesCacheKey = Effect.fn("makeClaudeCapabilitiesC
function* (
config: Pick<ClaudeSettings, "binaryPath" | "homePath">,
cwd?: string,
environment?: NodeJS.ProcessEnv,
): Effect.fn.Return<string, never, Path.Path> {
const resolvedHomePath = yield* resolveClaudeHomePath(config);
const resolvedHomePath = yield* resolveClaudeHomePath(config, environment);
return `${config.binaryPath}\0${resolvedHomePath}\0${cwd ?? ""}`;
},
);
Expand Down
Loading