diff --git a/apps/server/src/provider/Drivers/ClaudeHome.test.ts b/apps/server/src/provider/Drivers/ClaudeHome.test.ts index 87298f80125..97a71fb6e89 100644 --- a/apps/server/src/provider/Drivers/ClaudeHome.test.ts +++ b/apps/server/src/provider/Drivers/ClaudeHome.test.ts @@ -31,7 +31,7 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => { const resolved = path.resolve(NodeOS.homedir(), ".claude-work"); expect(yield* resolveClaudeHomePath({ homePath })).toBe(resolved); - expect((yield* makeClaudeEnvironment({ homePath })).HOME).toBe(resolved); + expect((yield* makeClaudeEnvironment({ homePath })).CLAUDE_CONFIG_DIR).toBe(resolved); expect(yield* makeClaudeContinuationGroupKey({ homePath })).toBe(`claude:home:${resolved}`); expect(yield* makeClaudeCapabilitiesCacheKey({ binaryPath: "claude", homePath })).toBe( `claude\0${resolved}`, diff --git a/apps/server/src/provider/Drivers/ClaudeHome.ts b/apps/server/src/provider/Drivers/ClaudeHome.ts index 65c74f9764a..f7d1ee539ac 100644 --- a/apps/server/src/provider/Drivers/ClaudeHome.ts +++ b/apps/server/src/provider/Drivers/ClaudeHome.ts @@ -24,7 +24,13 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function const resolvedHomePath = yield* resolveClaudeHomePath(config); return { ...resolvedBaseEnv, - HOME: resolvedHomePath, + // Isolate this instance's config via CLAUDE_CONFIG_DIR rather than HOME. + // Overriding HOME also relocates the macOS login keychain lookup + // ($HOME/Library/Keychains), so the spawned CLI can't find its stored + // OAuth credentials and reports "Not logged in". CLAUDE_CONFIG_DIR points + // Claude Code at its config dir directly while leaving HOME (and the + // keychain) intact. + CLAUDE_CONFIG_DIR: resolvedHomePath, }; }); diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index 191bf8e27db..17aeff2d0e3 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -418,7 +418,7 @@ describe("ClaudeAdapterLive", () => { ); }); - it.effect("runs Claude SDK sessions with the configured Claude HOME", () => { + it.effect("runs Claude SDK sessions with the configured CLAUDE_CONFIG_DIR", () => { const harness = makeHarness({ claudeConfig: { homePath: "~/.claude-work" } }); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; @@ -433,7 +433,10 @@ describe("ClaudeAdapterLive", () => { }); const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.env?.HOME, NodePath.join(NodeOS.homedir(), ".claude-work")); + assert.equal( + createInput?.options.env?.CLAUDE_CONFIG_DIR, + NodePath.join(NodeOS.homedir(), ".claude-work"), + ); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index b3ab1145495..05b1db20aad 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -1719,8 +1719,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te ), ); - it.effect("runs Claude status probes with the configured Claude HOME", () => { - const claudeHome = "/tmp/t3code-claude-home"; + it.effect("runs Claude status probes with the configured CLAUDE_CONFIG_DIR", () => { + const claudeConfigDir = "/tmp/t3code-claude-home"; const recorded = recordingMockSpawnerLayer((args) => { const joined = args.join(" "); if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 }; @@ -1737,14 +1737,14 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te const status = yield* checkClaudeProviderStatus( { ...defaultClaudeSettings, - homePath: claudeHome, + homePath: claudeConfigDir, }, claudeCapabilities(), ); assert.strictEqual(status.status, "ready"); assert.deepStrictEqual( - recorded.commands.map((command) => command.env?.HOME), - [claudeHome], + recorded.commands.map((command) => command.env?.CLAUDE_CONFIG_DIR), + [claudeConfigDir], ); }).pipe(Effect.provide(recorded.layer)); }); diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts index c8fe4ead3be..0f3905a0cb1 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts @@ -51,8 +51,8 @@ function makeFakeClaudeBinary(dir: string) { " exit 4", " }", "fi", - 'if [ -n "$T3_FAKE_CLAUDE_HOME_MUST_BE" ] && [ "$HOME" != "$T3_FAKE_CLAUDE_HOME_MUST_BE" ]; then', - ' printf "%s\\n" "HOME was $HOME" >&2', + 'if [ -n "$T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE" ] && [ "$CLAUDE_CONFIG_DIR" != "$T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE" ]; then', + ' printf "%s\\n" "CLAUDE_CONFIG_DIR was $CLAUDE_CONFIG_DIR" >&2', " exit 5", "fi", 'if [ -n "$T3_FAKE_CLAUDE_STDERR" ]; then', @@ -76,7 +76,7 @@ function withFakeClaudeEnv( argsMustContain?: string; argsMustNotContain?: string; stdinMustContain?: string; - homeMustBe?: string; + configDirMustBe?: string; claudeConfig?: Partial; }, effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect, @@ -92,7 +92,7 @@ function withFakeClaudeEnv( const previousArgsMustContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN; const previousArgsMustNotContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN; const previousStdinMustContain = process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN; - const previousHomeMustBe = process.env.T3_FAKE_CLAUDE_HOME_MUST_BE; + const previousConfigDirMustBe = process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE; yield* Effect.acquireRelease( Effect.sync(() => { @@ -129,10 +129,10 @@ function withFakeClaudeEnv( delete process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN; } - if (input.homeMustBe !== undefined) { - process.env.T3_FAKE_CLAUDE_HOME_MUST_BE = input.homeMustBe; + if (input.configDirMustBe !== undefined) { + process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE = input.configDirMustBe; } else { - delete process.env.T3_FAKE_CLAUDE_HOME_MUST_BE; + delete process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE; } }), () => @@ -175,10 +175,10 @@ function withFakeClaudeEnv( process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN = previousStdinMustContain; } - if (previousHomeMustBe === undefined) { - delete process.env.T3_FAKE_CLAUDE_HOME_MUST_BE; + if (previousConfigDirMustBe === undefined) { + delete process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE; } else { - process.env.T3_FAKE_CLAUDE_HOME_MUST_BE = previousHomeMustBe; + process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE = previousConfigDirMustBe; } }), ); @@ -286,10 +286,10 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { ), ); - it.effect("runs Claude text generation with the configured Claude HOME", () => + it.effect("runs Claude text generation with the configured CLAUDE_CONFIG_DIR", () => Effect.gen(function* () { const path = yield* Path.Path; - const claudeHome = path.join(process.cwd(), ".claude-work-test"); + const claudeConfigDir = path.join(process.cwd(), ".claude-work-test"); return yield* withFakeClaudeEnv( { // @effect-diagnostics-next-line preferSchemaOverJson:off @@ -298,8 +298,8 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { title: "Use Claude home", }, }), - homeMustBe: claudeHome, - claudeConfig: { homePath: claudeHome }, + configDirMustBe: claudeConfigDir, + claudeConfig: { homePath: claudeConfigDir }, }, (textGeneration) => Effect.gen(function* () { diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 6ccd65533dd..773b4481844 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -218,10 +218,10 @@ export const ClaudeSettings = makeProviderSettingsSchema( homePath: TrimmedString.pipe( Schema.withDecodingDefault(Effect.succeed("")), Schema.annotateKey({ - title: "Claude HOME path", + title: "CLAUDE_CONFIG_DIR path", description: - "Custom HOME used when running this Claude instance. Keeps .claude.json and .claude separate.", - providerSettingsForm: { placeholder: "~", clearWhenEmpty: "omit" }, + "Custom Claude home and config directory. Keeps .claude.json and .claude separate.", + providerSettingsForm: { placeholder: "~/.claude", clearWhenEmpty: "omit" }, }), ), customModels: Schema.Array(Schema.String).pipe(