Skip to content
Merged
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
3 changes: 3 additions & 0 deletions apps/server/src/provider/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ const runClaudeCommand = Effect.fn("runClaudeCommand")(function* (
claudeSettings: ClaudeSettings,
args: ReadonlyArray<string>,
environment?: NodeJS.ProcessEnv,
cwd?: string,
) {
const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment);
const spawnCommand = yield* resolveSpawnCommand(claudeSettings.binaryPath, args, {
Expand All @@ -463,6 +464,7 @@ const runClaudeCommand = Effect.fn("runClaudeCommand")(function* (
const command = ChildProcess.make(spawnCommand.command, spawnCommand.args, {
env: claudeEnvironment,
shell: spawnCommand.shell,
...(cwd ? { cwd } : {}),
});
return yield* spawnAndCollect(claudeSettings.binaryPath, command);
});
Expand Down Expand Up @@ -533,6 +535,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
claudeSettings,
["--version"],
resolvedEnvironment,
cwd,
).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result);

if (Result.isFailure(versionProbe)) {
Expand Down
12 changes: 8 additions & 4 deletions apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const { cwd } = yield* ServerConfig.ServerConfig;
// Instance-level probes are not tied to a project, so they run in the
// server's own state dir. The server cwd is $HOME in packaged desktop
// builds, and `claude` wrappers that scope themselves to their cwd refuse
// to run there. Workspace probes use the thread cwd via `snapshotForCwd`.
const { stateDir } = yield* ServerConfig.ServerConfig;
const httpClient = yield* HttpClient.HttpClient;
const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;
const resetCreditCoordinator = yield* ResetCreditCoordinator.ResetCreditCoordinator;
Expand Down Expand Up @@ -204,7 +208,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
timeToLive: CAPABILITIES_PROBE_TTL,
lookup: () =>
Effect.all([
probeClaudeCapabilities(effectiveConfig, processEnv, cwd),
probeClaudeCapabilities(effectiveConfig, processEnv, stateDir),
ClaudeResetCredits.readClaudeOrganizationId(accountConfigPath),
]).pipe(
Effect.map(([capabilities, workspaceId]) =>
Expand All @@ -216,7 +220,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
});
const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(
effectiveConfig,
cwd,
stateDir,
processEnv,
);

Expand All @@ -230,7 +234,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
effectiveConfig,
() => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey),
processEnv,
cwd,
stateDir,
catalog,
scopedLimitNames,
(version) =>
Expand Down
34 changes: 34 additions & 0 deletions apps/server/src/provider/ProviderInstanceRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,40 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => {
}).pipe(Effect.provide(layerTest)),
);

it.live("runs Claude instance probes in the server state dir, not the server cwd", () =>
Effect.gen(function* () {
if (yield* HostProcess.isWindows) return;

const fixtures = yield* makeTildeProviderFixtures();
const { stateDir } = yield* ServerConfig.ServerConfig;
const claudeId = ProviderInstanceId.make("claude_state_dir");
const { registry } = yield* makeProviderInstanceRegistry<ClaudeDriverEnv>({
drivers: [ClaudeDriver],
configMap: {
[claudeId]: {
driver: ProviderDriverKind.make("claudeAgent"),
enabled: true,
// Packaged desktop builds start the server in $HOME, where some
// `claude` wrappers refuse to run.
environment: [{ name: "T3_CLAUDE_REQUIRED_CWD", value: stateDir, sensitive: false }],
config: makeClaudeConfig({
enabled: true,
binaryPath: fixtures.claudeBinaryPath,
homePath: fixtures.claudeHomePath,
}),
},
},
});
const claude = yield* registry.getInstance(claudeId);

expect(yield* claude!.snapshot.refresh).toMatchObject({
status: "ready",
version: "2.1.219",
auth: { status: "authenticated" },
});
}).pipe(Effect.provide(layerTest)),
);

const redeemClaudeReset = (claim: { result: string; usageFailsAfterClaim: boolean }) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#!/usr/bin/env node
import * as NodeFS from "node:fs";
import * as NodeReadline from "node:readline";
// Opt-in: act like a `claude` wrapper that only runs inside one directory.
const requiredCwd = process.env.T3_CLAUDE_REQUIRED_CWD;
if (requiredCwd && process.cwd() !== NodeFS.realpathSync(requiredCwd)) {
process.stderr.write(`claude: refusing to run outside ${requiredCwd}\n`);
process.exit(1);
}
if (process.argv.includes("--version")) {
process.stdout.write("claude 2.1.219\n");
process.exit(0);
Expand Down
Loading