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* (...@@ -455,6 +455,7 @@ const runClaudeCommand = Effect.fn("runClaudeCommand")(function* (
455 claudeSettings: ClaudeSettings,455 claudeSettings: ClaudeSettings,
456 args: ReadonlyArray<string>,456 args: ReadonlyArray<string>,
457 environment?: NodeJS.ProcessEnv,457 environment?: NodeJS.ProcessEnv,
458 cwd?: string,
458) {459) {
459 const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment);460 const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment);
460 const spawnCommand = yield* resolveSpawnCommand(claudeSettings.binaryPath, args, {461 const spawnCommand = yield* resolveSpawnCommand(claudeSettings.binaryPath, args, {
...@@ -463,6 +464,7 @@ const runClaudeCommand = Effect.fn("runClaudeCommand")(function* (...@@ -463,6 +464,7 @@ const runClaudeCommand = Effect.fn("runClaudeCommand")(function* (
463 const command = ChildProcess.make(spawnCommand.command, spawnCommand.args, {464 const command = ChildProcess.make(spawnCommand.command, spawnCommand.args, {
464 env: claudeEnvironment,465 env: claudeEnvironment,
465 shell: spawnCommand.shell,466 shell: spawnCommand.shell,
467 ...(cwd ? { cwd } : {}),
466 });468 });
467 return yield* spawnAndCollect(claudeSettings.binaryPath, command);469 return yield* spawnAndCollect(claudeSettings.binaryPath, command);
468});470});
...@@ -533,6 +535,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(...@@ -533,6 +535,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
533 claudeSettings,535 claudeSettings,
534 ["--version"],536 ["--version"],
535 resolvedEnvironment,537 resolvedEnvironment,
538 cwd,
536 ).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result);539 ).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result);
537540
538 if (Result.isFailure(versionProbe)) {541 if (Result.isFailure(versionProbe)) {
apps/server/src/provider/Drivers/ClaudeDriver.ts+8-4
...@@ -123,7 +123,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path....@@ -123,7 +123,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
123 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;123 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
124 const fileSystem = yield* FileSystem.FileSystem;124 const fileSystem = yield* FileSystem.FileSystem;
125 const path = yield* Path.Path;125 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;
127 const httpClient = yield* HttpClient.HttpClient;131 const httpClient = yield* HttpClient.HttpClient;
128 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;132 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;
129 const resetCreditCoordinator = yield* ResetCreditCoordinator.ResetCreditCoordinator;133 const resetCreditCoordinator = yield* ResetCreditCoordinator.ResetCreditCoordinator;
...@@ -204,7 +208,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path....@@ -204,7 +208,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
204 timeToLive: CAPABILITIES_PROBE_TTL,208 timeToLive: CAPABILITIES_PROBE_TTL,
205 lookup: () =>209 lookup: () =>
206 Effect.all([210 Effect.all([
207 probeClaudeCapabilities(effectiveConfig, processEnv, cwd),211 probeClaudeCapabilities(effectiveConfig, processEnv, stateDir),
208 ClaudeResetCredits.readClaudeOrganizationId(accountConfigPath),212 ClaudeResetCredits.readClaudeOrganizationId(accountConfigPath),
209 ]).pipe(213 ]).pipe(
210 Effect.map(([capabilities, workspaceId]) =>214 Effect.map(([capabilities, workspaceId]) =>
...@@ -216,7 +220,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path....@@ -216,7 +220,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
216 });220 });
217 const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(221 const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(
218 effectiveConfig,222 effectiveConfig,
219 cwd,223 stateDir,
220 processEnv,224 processEnv,
221 );225 );
222226
...@@ -230,7 +234,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path....@@ -230,7 +234,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
230 effectiveConfig,234 effectiveConfig,
231 () => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey),235 () => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey),
232 processEnv,236 processEnv,
233 cwd,237 stateDir,
234 catalog,238 catalog,
235 scopedLimitNames,239 scopedLimitNames,
236 (version) =>240 (version) =>
apps/server/src/provider/ProviderInstanceRegistry.test.ts+34
...@@ -437,6 +437,40 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => {...@@ -437,6 +437,40 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => {
437 }).pipe(Effect.provide(layerTest)),437 }).pipe(Effect.provide(layerTest)),
438 );438 );
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
440 const redeemClaudeReset = (claim: { result: string; usageFailsAfterClaim: boolean }) =>474 const redeemClaudeReset = (claim: { result: string; usageFailsAfterClaim: boolean }) =>
441 Effect.gen(function* () {475 Effect.gen(function* () {
442 const fs = yield* FileSystem.FileSystem;476 const fs = yield* FileSystem.FileSystem;
apps/server/src/provider/testing/ProviderInstanceRegistryLive.fixture.mjs+6
...@@ -1,6 +1,12 @@...@@ -1,6 +1,12 @@
1#!/usr/bin/env node1#!/usr/bin/env node
2import * as NodeFS from "node:fs";2import * as NodeFS from "node:fs";
3import * as NodeReadline from "node:readline";3import * 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}
4if (process.argv.includes("--version")) {10if (process.argv.includes("--version")) {
5 process.stdout.write("claude 2.1.219\n");11 process.stdout.write("claude 2.1.219\n");
6 process.exit(0);12 process.exit(0);