authorgravatar for mitkostoikov1988@gmail.comDimitar Stoykov <mitkostoikov1988@gmail.com> 2026-07-17 12:46:55+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-17 11:46:55+02:00
logeb733c10f270bcfc2638b72eb25a8eaa314cb08a
tree556d7331e16a1a1cd4f10144e2421fbdd9996d24
parent8a02c718619a9d4c6588d3ab7a89204023cfab62
signature Signed by PGP key B5690EEEBB952194

fix(server): use CLAUDE_CONFIG_DIR instead of HOME for Claude instanc… (#4017)

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");
3232
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});
3036
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 });
420420
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 });
434434
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 );
17211721
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;
9696
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 }
131131
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 }
177177
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 );
288288
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:off295 // @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(