authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-14 01:38:46-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 01:38:46-07:00
log233e70b3b2b42f52f12a8ad48089f648a1cb143d
tree7610277f3a583b6fac9733d6b7b39df69728dc92
parent19a67ef5211996a737d9f9fe608ab2c80f8a6714
signature Signed by PGP key B5690EEEBB952194

fix(server): Claude no longer shows unavailable when its health check can't run in $HOME (#19320)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

4 files changed, 51 insertions(+), 4 deletions(-)

apps/server/src/provider/ClaudeProvider.ts+3
......@@ -455,6 +455,7 @@ const runClaudeCommand = Effect.fn("runClaudeCommand")(function* (
455455 claudeSettings: ClaudeSettings,
456456 args: ReadonlyArray<string>,
457457 environment?: NodeJS.ProcessEnv,
458 cwd?: string,
458459) {
459460 const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment);
460461 const spawnCommand = yield* resolveSpawnCommand(claudeSettings.binaryPath, args, {
......@@ -463,6 +464,7 @@ const runClaudeCommand = Effect.fn("runClaudeCommand")(function* (
463464 const command = ChildProcess.make(spawnCommand.command, spawnCommand.args, {
464465 env: claudeEnvironment,
465466 shell: spawnCommand.shell,
467 ...(cwd ? { cwd } : {}),
466468 });
467469 return yield* spawnAndCollect(claudeSettings.binaryPath, command);
468470});
......@@ -533,6 +535,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
533535 claudeSettings,
534536 ["--version"],
535537 resolvedEnvironment,
538 cwd,
536539 ).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result);
537540
538541 if (Result.isFailure(versionProbe)) {
apps/server/src/provider/Drivers/ClaudeDriver.ts+8-4
......@@ -123,7 +123,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
123123 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
124124 const fileSystem = yield* FileSystem.FileSystem;
125125 const path = yield* Path.Path;
126 const { cwd } = yield* ServerConfig.ServerConfig;
126 // Instance-level probes are not tied to a project, so they run in the
127 // server's own state dir. The server cwd is $HOME in packaged desktop
128 // builds, and `claude` wrappers that scope themselves to their cwd refuse
129 // to run there. Workspace probes use the thread cwd via `snapshotForCwd`.
130 const { stateDir } = yield* ServerConfig.ServerConfig;
127131 const httpClient = yield* HttpClient.HttpClient;
128132 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;
129133 const resetCreditCoordinator = yield* ResetCreditCoordinator.ResetCreditCoordinator;
......@@ -204,7 +208,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
204208 timeToLive: CAPABILITIES_PROBE_TTL,
205209 lookup: () =>
206210 Effect.all([
207 probeClaudeCapabilities(effectiveConfig, processEnv, cwd),
211 probeClaudeCapabilities(effectiveConfig, processEnv, stateDir),
208212 ClaudeResetCredits.readClaudeOrganizationId(accountConfigPath),
209213 ]).pipe(
210214 Effect.map(([capabilities, workspaceId]) =>
......@@ -216,7 +220,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
216220 });
217221 const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(
218222 effectiveConfig,
219 cwd,
223 stateDir,
220224 processEnv,
221225 );
222226
......@@ -230,7 +234,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
230234 effectiveConfig,
231235 () => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey),
232236 processEnv,
233 cwd,
237 stateDir,
234238 catalog,
235239 scopedLimitNames,
236240 (version) =>
apps/server/src/provider/ProviderInstanceRegistry.test.ts+34
......@@ -437,6 +437,40 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => {
437437 }).pipe(Effect.provide(layerTest)),
438438 );
439439
440 it.live("runs Claude instance probes in the server state dir, not the server cwd", () =>
441 Effect.gen(function* () {
442 if (yield* HostProcess.isWindows) return;
443
444 const fixtures = yield* makeTildeProviderFixtures();
445 const { stateDir } = yield* ServerConfig.ServerConfig;
446 const claudeId = ProviderInstanceId.make("claude_state_dir");
447 const { registry } = yield* makeProviderInstanceRegistry<ClaudeDriverEnv>({
448 drivers: [ClaudeDriver],
449 configMap: {
450 [claudeId]: {
451 driver: ProviderDriverKind.make("claudeAgent"),
452 enabled: true,
453 // Packaged desktop builds start the server in $HOME, where some
454 // `claude` wrappers refuse to run.
455 environment: [{ name: "T3_CLAUDE_REQUIRED_CWD", value: stateDir, sensitive: false }],
456 config: makeClaudeConfig({
457 enabled: true,
458 binaryPath: fixtures.claudeBinaryPath,
459 homePath: fixtures.claudeHomePath,
460 }),
461 },
462 },
463 });
464 const claude = yield* registry.getInstance(claudeId);
465
466 expect(yield* claude!.snapshot.refresh).toMatchObject({
467 status: "ready",
468 version: "2.1.219",
469 auth: { status: "authenticated" },
470 });
471 }).pipe(Effect.provide(layerTest)),
472 );
473
440474 const redeemClaudeReset = (claim: { result: string; usageFailsAfterClaim: boolean }) =>
441475 Effect.gen(function* () {
442476 const fs = yield* FileSystem.FileSystem;
apps/server/src/provider/testing/ProviderInstanceRegistryLive.fixture.mjs+6
......@@ -1,6 +1,12 @@
11#!/usr/bin/env node
22import * as NodeFS from "node:fs";
33import * as NodeReadline from "node:readline";
4// Opt-in: act like a `claude` wrapper that only runs inside one directory.
5const requiredCwd = process.env.T3_CLAUDE_REQUIRED_CWD;
6if (requiredCwd && process.cwd() !== NodeFS.realpathSync(requiredCwd)) {
7 process.stderr.write(`claude: refusing to run outside ${requiredCwd}\n`);
8 process.exit(1);
9}
410if (process.argv.includes("--version")) {
511 process.stdout.write("claude 2.1.219\n");
612 process.exit(0);