Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/server/src/provider/Drivers/ClaudeHome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down
8 changes: 7 additions & 1 deletion apps/server/src/provider/Drivers/ClaudeHome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
dmstoykov marked this conversation as resolved.
};
});

Expand Down
7 changes: 5 additions & 2 deletions apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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),
Expand Down
10 changes: 5 additions & 5 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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));
});
Expand Down
28 changes: 14 additions & 14 deletions apps/server/src/textGeneration/ClaudeTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -76,7 +76,7 @@ function withFakeClaudeEnv<A, E, R>(
argsMustContain?: string;
argsMustNotContain?: string;
stdinMustContain?: string;
homeMustBe?: string;
configDirMustBe?: string;
claudeConfig?: Partial<ClaudeSettings>;
},
effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,
Expand All @@ -92,7 +92,7 @@ function withFakeClaudeEnv<A, E, R>(
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(() => {
Expand Down Expand Up @@ -129,10 +129,10 @@ function withFakeClaudeEnv<A, E, R>(
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;
}
}),
() =>
Expand Down Expand Up @@ -175,10 +175,10 @@ function withFakeClaudeEnv<A, E, R>(
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;
}
}),
);
Expand Down Expand Up @@ -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
Expand All @@ -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* () {
Expand Down
6 changes: 3 additions & 3 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading