| author | |
| committer | |
| log | eb733c10f270bcfc2638b72eb25a8eaa314cb08a |
| tree | 556d7331e16a1a1cd4f10144e2421fbdd9996d24 |
| parent | 8a02c718619a9d4c6588d3ab7a89204023cfab62 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>6 files changed, 35 insertions(+), 26 deletions(-)
apps/server/src/provider/Drivers/ClaudeHome.test.ts+1-1| ... | @@ -31,7 +31,7 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => { | ... | @@ -31,7 +31,7 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => { |
| 31 | const resolved = path.resolve(NodeOS.homedir(), ".claude-work"); | 31 | const resolved = path.resolve(NodeOS.homedir(), ".claude-work"); |
| 32 | 32 | ||
| 33 | expect(yield* resolveClaudeHomePath({ homePath })).toBe(resolved); | 33 | expect(yield* resolveClaudeHomePath({ homePath })).toBe(resolved); |
| 34 | expect((yield* makeClaudeEnvironment({ homePath })).HOME).toBe(resolved); | 34 | expect((yield* makeClaudeEnvironment({ homePath })).CLAUDE_CONFIG_DIR).toBe(resolved); |
| 35 | expect(yield* makeClaudeContinuationGroupKey({ homePath })).toBe(`claude:home:${resolved}`); | 35 | expect(yield* makeClaudeContinuationGroupKey({ homePath })).toBe(`claude:home:${resolved}`); |
| 36 | expect(yield* makeClaudeCapabilitiesCacheKey({ binaryPath: "claude", homePath })).toBe( | 36 | expect(yield* makeClaudeCapabilitiesCacheKey({ binaryPath: "claude", homePath })).toBe( |
| 37 | `claude\0${resolved}`, | 37 | `claude\0${resolved}`, |
apps/server/src/provider/Drivers/ClaudeHome.ts+7-1| ... | @@ -24,7 +24,13 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function | ... | @@ -24,7 +24,13 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function |
| 24 | const resolvedHomePath = yield* resolveClaudeHomePath(config); | 24 | const resolvedHomePath = yield* resolveClaudeHomePath(config); |
| 25 | return { | 25 | return { |
| 26 | ...resolvedBaseEnv, | 26 | ...resolvedBaseEnv, |
| 27 | HOME: resolvedHomePath, | 27 | // Isolate this instance's config via CLAUDE_CONFIG_DIR rather than HOME. |
| 28 | // Overriding HOME also relocates the macOS login keychain lookup | ||
| 29 | // ($HOME/Library/Keychains), so the spawned CLI can't find its stored | ||
| 30 | // OAuth credentials and reports "Not logged in". CLAUDE_CONFIG_DIR points | ||
| 31 | // Claude Code at its config dir directly while leaving HOME (and the | ||
| 32 | // keychain) intact. | ||
| 33 | CLAUDE_CONFIG_DIR: resolvedHomePath, | ||
| 28 | }; | 34 | }; |
| 29 | }); | 35 | }); |
| 30 | 36 |
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+5-2| ... | @@ -418,7 +418,7 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -418,7 +418,7 @@ describe("ClaudeAdapterLive", () => { |
| 418 | ); | 418 | ); |
| 419 | }); | 419 | }); |
| 420 | 420 | ||
| 421 | it.effect("runs Claude SDK sessions with the configured Claude HOME", () => { | 421 | it.effect("runs Claude SDK sessions with the configured CLAUDE_CONFIG_DIR", () => { |
| 422 | const harness = makeHarness({ claudeConfig: { homePath: "~/.claude-work" } }); | 422 | const harness = makeHarness({ claudeConfig: { homePath: "~/.claude-work" } }); |
| 423 | return Effect.gen(function* () { | 423 | return Effect.gen(function* () { |
| 424 | const adapter = yield* ClaudeAdapter; | 424 | const adapter = yield* ClaudeAdapter; |
| ... | @@ -433,7 +433,10 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -433,7 +433,10 @@ describe("ClaudeAdapterLive", () => { |
| 433 | }); | 433 | }); |
| 434 | 434 | ||
| 435 | const createInput = harness.getLastCreateQueryInput(); | 435 | const createInput = harness.getLastCreateQueryInput(); |
| 436 | assert.equal(createInput?.options.env?.HOME, NodePath.join(NodeOS.homedir(), ".claude-work")); | 436 | assert.equal( |
| 437 | createInput?.options.env?.CLAUDE_CONFIG_DIR, | ||
| 438 | NodePath.join(NodeOS.homedir(), ".claude-work"), | ||
| 439 | ); | ||
| 437 | }).pipe( | 440 | }).pipe( |
| 438 | Effect.provideService(Random.Random, makeDeterministicRandomService()), | 441 | Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| 439 | Effect.provide(harness.layer), | 442 | Effect.provide(harness.layer), |
apps/server/src/provider/Layers/ProviderRegistry.test.ts+5-5| ... | @@ -1719,8 +1719,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te | ... | @@ -1719,8 +1719,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1719 | ), | 1719 | ), |
| 1720 | ); | 1720 | ); |
| 1721 | 1721 | ||
| 1722 | it.effect("runs Claude status probes with the configured Claude HOME", () => { | 1722 | it.effect("runs Claude status probes with the configured CLAUDE_CONFIG_DIR", () => { |
| 1723 | const claudeHome = "/tmp/t3code-claude-home"; | 1723 | const claudeConfigDir = "/tmp/t3code-claude-home"; |
| 1724 | const recorded = recordingMockSpawnerLayer((args) => { | 1724 | const recorded = recordingMockSpawnerLayer((args) => { |
| 1725 | const joined = args.join(" "); | 1725 | const joined = args.join(" "); |
| 1726 | if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 }; | 1726 | 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 | ... | @@ -1737,14 +1737,14 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1737 | const status = yield* checkClaudeProviderStatus( | 1737 | const status = yield* checkClaudeProviderStatus( |
| 1738 | { | 1738 | { |
| 1739 | ...defaultClaudeSettings, | 1739 | ...defaultClaudeSettings, |
| 1740 | homePath: claudeHome, | 1740 | homePath: claudeConfigDir, |
| 1741 | }, | 1741 | }, |
| 1742 | claudeCapabilities(), | 1742 | claudeCapabilities(), |
| 1743 | ); | 1743 | ); |
| 1744 | assert.strictEqual(status.status, "ready"); | 1744 | assert.strictEqual(status.status, "ready"); |
| 1745 | assert.deepStrictEqual( | 1745 | assert.deepStrictEqual( |
| 1746 | recorded.commands.map((command) => command.env?.HOME), | 1746 | recorded.commands.map((command) => command.env?.CLAUDE_CONFIG_DIR), |
| 1747 | [claudeHome], | 1747 | [claudeConfigDir], |
| 1748 | ); | 1748 | ); |
| 1749 | }).pipe(Effect.provide(recorded.layer)); | 1749 | }).pipe(Effect.provide(recorded.layer)); |
| 1750 | }); | 1750 | }); |
apps/server/src/textGeneration/ClaudeTextGeneration.test.ts+14-14| ... | @@ -51,8 +51,8 @@ function makeFakeClaudeBinary(dir: string) { | ... | @@ -51,8 +51,8 @@ function makeFakeClaudeBinary(dir: string) { |
| 51 | " exit 4", | 51 | " exit 4", |
| 52 | " }", | 52 | " }", |
| 53 | "fi", | 53 | "fi", |
| 54 | 'if [ -n "$T3_FAKE_CLAUDE_HOME_MUST_BE" ] && [ "$HOME" != "$T3_FAKE_CLAUDE_HOME_MUST_BE" ]; then', | 54 | 'if [ -n "$T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE" ] && [ "$CLAUDE_CONFIG_DIR" != "$T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE" ]; then', |
| 55 | ' printf "%s\\n" "HOME was $HOME" >&2', | 55 | ' printf "%s\\n" "CLAUDE_CONFIG_DIR was $CLAUDE_CONFIG_DIR" >&2', |
| 56 | " exit 5", | 56 | " exit 5", |
| 57 | "fi", | 57 | "fi", |
| 58 | 'if [ -n "$T3_FAKE_CLAUDE_STDERR" ]; then', | 58 | 'if [ -n "$T3_FAKE_CLAUDE_STDERR" ]; then', |
| ... | @@ -76,7 +76,7 @@ function withFakeClaudeEnv<A, E, R>( | ... | @@ -76,7 +76,7 @@ function withFakeClaudeEnv<A, E, R>( |
| 76 | argsMustContain?: string; | 76 | argsMustContain?: string; |
| 77 | argsMustNotContain?: string; | 77 | argsMustNotContain?: string; |
| 78 | stdinMustContain?: string; | 78 | stdinMustContain?: string; |
| 79 | homeMustBe?: string; | 79 | configDirMustBe?: string; |
| 80 | claudeConfig?: Partial<ClaudeSettings>; | 80 | claudeConfig?: Partial<ClaudeSettings>; |
| 81 | }, | 81 | }, |
| 82 | effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>, | 82 | effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>, |
| ... | @@ -92,7 +92,7 @@ function withFakeClaudeEnv<A, E, R>( | ... | @@ -92,7 +92,7 @@ function withFakeClaudeEnv<A, E, R>( |
| 92 | const previousArgsMustContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN; | 92 | const previousArgsMustContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN; |
| 93 | const previousArgsMustNotContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN; | 93 | const previousArgsMustNotContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN; |
| 94 | const previousStdinMustContain = process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN; | 94 | const previousStdinMustContain = process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN; |
| 95 | const previousHomeMustBe = process.env.T3_FAKE_CLAUDE_HOME_MUST_BE; | 95 | const previousConfigDirMustBe = process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE; |
| 96 | 96 | ||
| 97 | yield* Effect.acquireRelease( | 97 | yield* Effect.acquireRelease( |
| 98 | Effect.sync(() => { | 98 | Effect.sync(() => { |
| ... | @@ -129,10 +129,10 @@ function withFakeClaudeEnv<A, E, R>( | ... | @@ -129,10 +129,10 @@ function withFakeClaudeEnv<A, E, R>( |
| 129 | delete process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN; | 129 | delete process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN; |
| 130 | } | 130 | } |
| 131 | 131 | ||
| 132 | if (input.homeMustBe !== undefined) { | 132 | if (input.configDirMustBe !== undefined) { |
| 133 | process.env.T3_FAKE_CLAUDE_HOME_MUST_BE = input.homeMustBe; | 133 | process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE = input.configDirMustBe; |
| 134 | } else { | 134 | } else { |
| 135 | delete process.env.T3_FAKE_CLAUDE_HOME_MUST_BE; | 135 | delete process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE; |
| 136 | } | 136 | } |
| 137 | }), | 137 | }), |
| 138 | () => | 138 | () => |
| ... | @@ -175,10 +175,10 @@ function withFakeClaudeEnv<A, E, R>( | ... | @@ -175,10 +175,10 @@ function withFakeClaudeEnv<A, E, R>( |
| 175 | process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN = previousStdinMustContain; | 175 | process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN = previousStdinMustContain; |
| 176 | } | 176 | } |
| 177 | 177 | ||
| 178 | if (previousHomeMustBe === undefined) { | 178 | if (previousConfigDirMustBe === undefined) { |
| 179 | delete process.env.T3_FAKE_CLAUDE_HOME_MUST_BE; | 179 | delete process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE; |
| 180 | } else { | 180 | } else { |
| 181 | process.env.T3_FAKE_CLAUDE_HOME_MUST_BE = previousHomeMustBe; | 181 | process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE = previousConfigDirMustBe; |
| 182 | } | 182 | } |
| 183 | }), | 183 | }), |
| 184 | ); | 184 | ); |
| ... | @@ -286,10 +286,10 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { | ... | @@ -286,10 +286,10 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { |
| 286 | ), | 286 | ), |
| 287 | ); | 287 | ); |
| 288 | 288 | ||
| 289 | it.effect("runs Claude text generation with the configured Claude HOME", () => | 289 | it.effect("runs Claude text generation with the configured CLAUDE_CONFIG_DIR", () => |
| 290 | Effect.gen(function* () { | 290 | Effect.gen(function* () { |
| 291 | const path = yield* Path.Path; | 291 | const path = yield* Path.Path; |
| 292 | const claudeHome = path.join(process.cwd(), ".claude-work-test"); | 292 | const claudeConfigDir = path.join(process.cwd(), ".claude-work-test"); |
| 293 | return yield* withFakeClaudeEnv( | 293 | return yield* withFakeClaudeEnv( |
| 294 | { | 294 | { |
| 295 | // @effect-diagnostics-next-line preferSchemaOverJson:off | 295 | // @effect-diagnostics-next-line preferSchemaOverJson:off |
| ... | @@ -298,8 +298,8 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { | ... | @@ -298,8 +298,8 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { |
| 298 | title: "Use Claude home", | 298 | title: "Use Claude home", |
| 299 | }, | 299 | }, |
| 300 | }), | 300 | }), |
| 301 | homeMustBe: claudeHome, | 301 | configDirMustBe: claudeConfigDir, |
| 302 | claudeConfig: { homePath: claudeHome }, | 302 | claudeConfig: { homePath: claudeConfigDir }, |
| 303 | }, | 303 | }, |
| 304 | (textGeneration) => | 304 | (textGeneration) => |
| 305 | Effect.gen(function* () { | 305 | Effect.gen(function* () { |
packages/contracts/src/settings.ts+3-3| ... | @@ -218,10 +218,10 @@ export const ClaudeSettings = makeProviderSettingsSchema( | ... | @@ -218,10 +218,10 @@ export const ClaudeSettings = makeProviderSettingsSchema( |
| 218 | homePath: TrimmedString.pipe( | 218 | homePath: TrimmedString.pipe( |
| 219 | Schema.withDecodingDefault(Effect.succeed("")), | 219 | Schema.withDecodingDefault(Effect.succeed("")), |
| 220 | Schema.annotateKey({ | 220 | Schema.annotateKey({ |
| 221 | title: "Claude HOME path", | 221 | title: "CLAUDE_CONFIG_DIR path", |
| 222 | description: | 222 | description: |
| 223 | "Custom HOME used when running this Claude instance. Keeps .claude.json and .claude separate.", | 223 | "Custom Claude home and config directory. Keeps .claude.json and .claude separate.", |
| 224 | providerSettingsForm: { placeholder: "~", clearWhenEmpty: "omit" }, | 224 | providerSettingsForm: { placeholder: "~/.claude", clearWhenEmpty: "omit" }, |
| 225 | }), | 225 | }), |
| 226 | ), | 226 | ), |
| 227 | customModels: Schema.Array(Schema.String).pipe( | 227 | customModels: Schema.Array(Schema.String).pipe( |