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) => {
3131 const resolved = path.resolve(NodeOS.homedir(), ".claude-work");
3232
3333 expect(yield* resolveClaudeHomePath({ homePath })).toBe(resolved);
34 expect((yield* makeClaudeEnvironment({ homePath })).HOME).toBe(resolved);
34 expect((yield* makeClaudeEnvironment({ homePath })).CLAUDE_CONFIG_DIR).toBe(resolved);
3535 expect(yield* makeClaudeContinuationGroupKey({ homePath })).toBe(`claude:home:${resolved}`);
3636 expect(yield* makeClaudeCapabilitiesCacheKey({ binaryPath: "claude", homePath })).toBe(
3737 `claude\0${resolved}`,
apps/server/src/provider/Drivers/ClaudeHome.ts+7-1
......@@ -24,7 +24,13 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function
2424 const resolvedHomePath = yield* resolveClaudeHomePath(config);
2525 return {
2626 ...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,
2834 };
2935});
3036
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+5-2
......@@ -418,7 +418,7 @@ describe("ClaudeAdapterLive", () => {
418418 );
419419 });
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", () => {
422422 const harness = makeHarness({ claudeConfig: { homePath: "~/.claude-work" } });
423423 return Effect.gen(function* () {
424424 const adapter = yield* ClaudeAdapter;
......@@ -433,7 +433,10 @@ describe("ClaudeAdapterLive", () => {
433433 });
434434
435435 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 );
437440 }).pipe(
438441 Effect.provideService(Random.Random, makeDeterministicRandomService()),
439442 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
17191719 ),
17201720 );
17211721
1722 it.effect("runs Claude status probes with the configured Claude HOME", () => {
1723 const claudeHome = "/tmp/t3code-claude-home";
1722 it.effect("runs Claude status probes with the configured CLAUDE_CONFIG_DIR", () => {
1723 const claudeConfigDir = "/tmp/t3code-claude-home";
17241724 const recorded = recordingMockSpawnerLayer((args) => {
17251725 const joined = args.join(" ");
17261726 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
17371737 const status = yield* checkClaudeProviderStatus(
17381738 {
17391739 ...defaultClaudeSettings,
1740 homePath: claudeHome,
1740 homePath: claudeConfigDir,
17411741 },
17421742 claudeCapabilities(),
17431743 );
17441744 assert.strictEqual(status.status, "ready");
17451745 assert.deepStrictEqual(
1746 recorded.commands.map((command) => command.env?.HOME),
1747 [claudeHome],
1746 recorded.commands.map((command) => command.env?.CLAUDE_CONFIG_DIR),
1747 [claudeConfigDir],
17481748 );
17491749 }).pipe(Effect.provide(recorded.layer));
17501750 });
apps/server/src/textGeneration/ClaudeTextGeneration.test.ts+14-14
......@@ -51,8 +51,8 @@ function makeFakeClaudeBinary(dir: string) {
5151 " exit 4",
5252 " }",
5353 "fi",
54 'if [ -n "$T3_FAKE_CLAUDE_HOME_MUST_BE" ] && [ "$HOME" != "$T3_FAKE_CLAUDE_HOME_MUST_BE" ]; then',
55 ' printf "%s\\n" "HOME was $HOME" >&2',
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" "CLAUDE_CONFIG_DIR was $CLAUDE_CONFIG_DIR" >&2',
5656 " exit 5",
5757 "fi",
5858 'if [ -n "$T3_FAKE_CLAUDE_STDERR" ]; then',
......@@ -76,7 +76,7 @@ function withFakeClaudeEnv<A, E, R>(
7676 argsMustContain?: string;
7777 argsMustNotContain?: string;
7878 stdinMustContain?: string;
79 homeMustBe?: string;
79 configDirMustBe?: string;
8080 claudeConfig?: Partial<ClaudeSettings>;
8181 },
8282 effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,
......@@ -92,7 +92,7 @@ function withFakeClaudeEnv<A, E, R>(
9292 const previousArgsMustContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN;
9393 const previousArgsMustNotContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN;
9494 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
9797 yield* Effect.acquireRelease(
9898 Effect.sync(() => {
......@@ -129,10 +129,10 @@ function withFakeClaudeEnv<A, E, R>(
129129 delete process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN;
130130 }
131131
132 if (input.homeMustBe !== undefined) {
133 process.env.T3_FAKE_CLAUDE_HOME_MUST_BE = input.homeMustBe;
132 if (input.configDirMustBe !== undefined) {
133 process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE = input.configDirMustBe;
134134 } else {
135 delete process.env.T3_FAKE_CLAUDE_HOME_MUST_BE;
135 delete process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE;
136136 }
137137 }),
138138 () =>
......@@ -175,10 +175,10 @@ function withFakeClaudeEnv<A, E, R>(
175175 process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN = previousStdinMustContain;
176176 }
177177
178 if (previousHomeMustBe === undefined) {
179 delete process.env.T3_FAKE_CLAUDE_HOME_MUST_BE;
178 if (previousConfigDirMustBe === undefined) {
179 delete process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE;
180180 } else {
181 process.env.T3_FAKE_CLAUDE_HOME_MUST_BE = previousHomeMustBe;
181 process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE = previousConfigDirMustBe;
182182 }
183183 }),
184184 );
......@@ -286,10 +286,10 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => {
286286 ),
287287 );
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", () =>
290290 Effect.gen(function* () {
291291 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");
293293 return yield* withFakeClaudeEnv(
294294 {
295295 // @effect-diagnostics-next-line preferSchemaOverJson:off
......@@ -298,8 +298,8 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => {
298298 title: "Use Claude home",
299299 },
300300 }),
301 homeMustBe: claudeHome,
302 claudeConfig: { homePath: claudeHome },
301 configDirMustBe: claudeConfigDir,
302 claudeConfig: { homePath: claudeConfigDir },
303303 },
304304 (textGeneration) =>
305305 Effect.gen(function* () {
packages/contracts/src/settings.ts+3-3
......@@ -218,10 +218,10 @@ export const ClaudeSettings = makeProviderSettingsSchema(
218218 homePath: TrimmedString.pipe(
219219 Schema.withDecodingDefault(Effect.succeed("")),
220220 Schema.annotateKey({
221 title: "Claude HOME path",
221 title: "CLAUDE_CONFIG_DIR path",
222222 description:
223 "Custom HOME used when running this Claude instance. Keeps .claude.json and .claude separate.",
224 providerSettingsForm: { placeholder: "~", clearWhenEmpty: "omit" },
223 "Custom Claude home and config directory. Keeps .claude.json and .claude separate.",
224 providerSettingsForm: { placeholder: "~/.claude", clearWhenEmpty: "omit" },
225225 }),
226226 ),
227227 customModels: Schema.Array(Schema.String).pipe(