From c3a727c6cc94fee6258c084a3efc611647d75d4f Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 16:51:42 -0500 Subject: [PATCH 1/6] fix(claude): live Bash output finds Claude's task file when TMPDIR is set Claude Code writes each running command's output under CLAUDE_CODE_TMPDIR or os.tmpdir(), which reads TMPDIR, TMP and TEMP. The tail only checked CLAUDE_CODE_TMPDIR and otherwise looked in /tmp, so on macOS (launchd always sets TMPDIR) and on Linux hosts with TMPDIR set, a foreground command's output never streamed while it ran. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/ClaudeTaskOutputTail.test.ts | 22 ++++++++++++++++++- .../Adapters/ClaudeTaskOutputTail.ts | 9 +++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts index 2a06d25c5030d..ffd1f9f42c333 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts @@ -7,7 +7,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; -import { tailClaudeTaskOutput } from "./ClaudeTaskOutputTail.ts"; +import { claudeTaskOutputRoot, tailClaudeTaskOutput } from "./ClaudeTaskOutputTail.ts"; const SESSION_ID = "0c91bb28-9e89-4889-bd22-3de9880233d3"; const TASK_ID = "bk6xreeye"; @@ -106,3 +106,23 @@ describe("tailClaudeTaskOutput", () => { ), ); }); + +describe("claudeTaskOutputRoot", () => { + it.effect.skipIf(process.getuid === undefined)( + "uses the temp dir Claude Code resolves from its environment", + () => + Effect.gen(function* () { + const path = yield* Path.Path; + const user = `claude-${process.getuid?.()}`; + const root = (environment: Record) => + claudeTaskOutputRoot(environment, path); + assert.equal(root({ TMPDIR: "/var/folders/x/T/" }), `/var/folders/x/T/${user}`); + assert.equal(root({ TMP: "/tmp/user/1000" }), `/tmp/user/1000/${user}`); + assert.equal( + root({ CLAUDE_CODE_TMPDIR: "/srv/claude", TMPDIR: "/var/folders/x/T/" }), + `/srv/claude/${user}`, + ); + assert.equal(root({}), `/tmp/${user}`); + }).pipe(Effect.provide(NodeServices.layer)), + ); +}); diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts index fbe759e973a40..253591ba9e61c 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts @@ -29,7 +29,14 @@ export function claudeTaskOutputRoot( ): string | undefined { const uid = process.getuid?.(); if (uid === undefined) return undefined; - const base = environment?.CLAUDE_CODE_TMPDIR?.trim() || "/tmp"; + // Claude Code falls back to `os.tmpdir()`, which reads these from the + // environment it was launched with: macOS always sets TMPDIR. + const base = + environment?.CLAUDE_CODE_TMPDIR?.trim() || + environment?.TMPDIR || + environment?.TMP || + environment?.TEMP || + "/tmp"; return path.join(base, `claude-${uid}`); } From 96676516686ef29e568647b4984b13be3356b787 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Fri, 23 Oct 2026 03:09:53 -0500 Subject: [PATCH 2/6] fix(claude): resolve a relative temp dir against Claude's working directory Claude Code runs in the project directory, so a relative TMPDIR, TMP, TEMP or CLAUDE_CODE_TMPDIR points under the project. The tail resolved it against the server's own working directory and missed the task file. Co-Authored-By: Claude Opus 5.5 --- .../orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts | 2 +- .../src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 6 +++++- .../Adapters/ClaudeTaskOutputTail.test.ts | 9 +++++++-- .../orchestration-v2/Adapters/ClaudeTaskOutputTail.ts | 6 ++++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts index ec3ec5b052461..542f6019126af 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts @@ -1024,7 +1024,7 @@ const writeReplayTaskOutputFiles = Effect.fn("ClaudeReplay.writeTaskOutputFiles" const stderr = typeof result.stderr === "string" ? result.stderr : ""; outputByToolUseId.set(toolResult.tool_use_id, `${result.stdout}${stderr}`); } - const root = claudeTaskOutputRoot({ CLAUDE_CODE_TMPDIR: tmpRoot }, path); + const root = claudeTaskOutputRoot({ CLAUDE_CODE_TMPDIR: tmpRoot }, path, null); for (const frame of frames) { if (frame.type !== "system" || frame.subtype !== "task_started") continue; if (frame.task_type !== "local_bash" || frame.is_backgrounded !== false) continue; diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 49b344961b3fb..8ad83d1489a61 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -4016,7 +4016,11 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( string, { readonly fiber: Fiber.Fiber; readonly stop: Deferred.Deferred } >(); - const taskOutputRoot = claudeTaskOutputRoot(adapterOptions.environment, path); + const taskOutputRoot = claudeTaskOutputRoot( + adapterOptions.environment, + path, + input.runtimePolicy.cwd, + ); // Waits for the tail's last read so its output lands before the call settles. const stopTaskOutputTail = (toolUseId: string) => Effect.suspend(() => { diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts index ffd1f9f42c333..77a6f79b640de 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts @@ -114,8 +114,8 @@ describe("claudeTaskOutputRoot", () => { Effect.gen(function* () { const path = yield* Path.Path; const user = `claude-${process.getuid?.()}`; - const root = (environment: Record) => - claudeTaskOutputRoot(environment, path); + const root = (environment: Record, cwd: string | null = null) => + claudeTaskOutputRoot(environment, path, cwd); assert.equal(root({ TMPDIR: "/var/folders/x/T/" }), `/var/folders/x/T/${user}`); assert.equal(root({ TMP: "/tmp/user/1000" }), `/tmp/user/1000/${user}`); assert.equal( @@ -123,6 +123,11 @@ describe("claudeTaskOutputRoot", () => { `/srv/claude/${user}`, ); assert.equal(root({}), `/tmp/${user}`); + assert.equal(root({ TMPDIR: "tmp" }, "/work/project"), `/work/project/tmp/${user}`); + assert.equal( + root({ CLAUDE_CODE_TMPDIR: "../claude" }, "/work/project"), + `/work/claude/${user}`, + ); }).pipe(Effect.provide(NodeServices.layer)), ); }); diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts index 253591ba9e61c..9c8730da8372b 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts @@ -26,18 +26,20 @@ const MAX_READ_BYTES = 256 * 1024; export function claudeTaskOutputRoot( environment: Readonly> | undefined, path: Path.Path, + cwd: string | null, ): string | undefined { const uid = process.getuid?.(); if (uid === undefined) return undefined; // Claude Code falls back to `os.tmpdir()`, which reads these from the - // environment it was launched with: macOS always sets TMPDIR. + // environment it was launched with: macOS always sets TMPDIR. A relative + // value is relative to Claude's working directory, not this server's. const base = environment?.CLAUDE_CODE_TMPDIR?.trim() || environment?.TMPDIR || environment?.TMP || environment?.TEMP || "/tmp"; - return path.join(base, `claude-${uid}`); + return path.resolve(cwd ?? ".", base, `claude-${uid}`); } const locate = Effect.fn("ClaudeTaskOutputTail.locate")(function* (input: { From 5cc9d0625980e718a41519df6d139615b168c505 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Fri, 23 Oct 2026 04:06:00 -0500 Subject: [PATCH 3/6] fix(claude): find live Bash output under the cwd its Claude process runs in The task-output root was computed once per provider session from the session's cwd. A turn can launch Claude in a different cwd on the same session, so with a relative temp dir the tail looked under the wrong directory. Each Claude process now carries its own root, built from the cwd it was launched with, and a reused or continued process keeps it. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/ClaudeAdapterV2.test.ts | 103 ++++++++++++++++++ .../Adapters/ClaudeAdapterV2.ts | 17 +-- 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index b231fbfca9d98..54cc4973ac365 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -6410,6 +6410,109 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + it.effect.skipIf(process.getuid === undefined)( + "tails foreground Bash output under the cwd the query launched in, not the session's", + () => + Effect.scoped( + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const launchCwd = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-claude-task-output-cwd-", + }); + // The session opens in /workspace; a relative TMPDIR puts Claude's + // task files under whichever cwd the CLI was launched in. + const harness = yield* makeWakeHarnessWithOptions({ environment: { TMPDIR: "tmp" } }); + const now = yield* DateTime.now; + const toolUseId = "toolu_foreground_bash"; + const taskId = "foreground-bash-task"; + const tasks = path.join( + launchCwd, + "tmp", + `claude-${process.getuid?.()}`, + "project", + WAKE_NATIVE_SESSION, + "tasks", + ); + yield* fileSystem.makeDirectory(tasks, { recursive: true }); + yield* fileSystem.writeFileString(path.join(tasks, `${taskId}.output`), "line 1\n"); + + yield* harness.runtime.startTurn( + makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now, + attemptId: RunAttemptId.make("attempt-claude-task-output-cwd"), + text: "Run the build.", + attachments: [], + runtimePolicy: { ...CLAUDE_TEST_RUNTIME_POLICY, cwd: launchCwd }, + }), + ); + assert.equal(harness.getOpenedOptions()?.cwd, launchCwd); + for (const frame of [ + claudeSdkFrame({ + type: "assistant", + message: { + model: "claude-sonnet-4-6", + id: "msg_foreground_bash", + type: "message", + role: "assistant", + content: [ + { + type: "tool_use", + id: toolUseId, + name: "Bash", + input: { command: "vp run build", description: "Build" }, + }, + ], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 1, output_tokens: 1 }, + }, + parent_tool_use_id: null, + uuid: "00000000-0000-4000-8000-000000000a01", + session_id: WAKE_NATIVE_SESSION, + }), + claudeSdkFrame({ + type: "system", + subtype: "task_started", + task_id: taskId, + tool_use_id: toolUseId, + description: "vp run build", + is_backgrounded: false, + task_type: "local_bash", + uuid: "00000000-0000-4000-8000-000000000a02", + session_id: WAKE_NATIVE_SESSION, + }), + claudeSdkFrame({ + type: "user", + message: { + role: "user", + content: [{ type: "tool_result", tool_use_id: toolUseId, content: "line 1" }], + }, + parent_tool_use_id: null, + uuid: "00000000-0000-4000-8000-000000000a03", + session_id: WAKE_NATIVE_SESSION, + tool_use_result: { stdout: "line 1\n", stderr: "", interrupted: false }, + }), + makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000a04", result: "Built." }), + ]) { + yield* Queue.offer(harness.sdkMessages, frame); + } + yield* harness.awaitTerminal(1); + + const chunks = harness.events.flatMap((event) => + event.type === "command_output.delta" ? [event.chunk] : [], + ); + assert.deepEqual(chunks, ["line 1\n"]); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + it.effect("stops background work after the turn settled", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 8ad83d1489a61..700117080f5d7 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -3501,6 +3501,9 @@ interface ClaudeLiveQueryContext { permissionMode: PermissionMode; stopping: boolean; readonly subagentsFromEarlierProcesses: ReadonlySet; + // Per process, not per session: a relative temp dir follows the cwd this CLI + // was launched in, and a later turn's cwd can differ. + readonly taskOutputRoot: string | undefined; } interface ActiveClaudeToolCall { @@ -4016,11 +4019,6 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( string, { readonly fiber: Fiber.Fiber; readonly stop: Deferred.Deferred } >(); - const taskOutputRoot = claudeTaskOutputRoot( - adapterOptions.environment, - path, - input.runtimePolicy.cwd, - ); // Waits for the tail's last read so its output lands before the call settles. const stopTaskOutputTail = (toolUseId: string) => Effect.suspend(() => { @@ -8051,7 +8049,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( ? undefined : findToolCall(context, message.tool_use_id); if ( - taskOutputRoot !== undefined && + liveQuery.taskOutputRoot !== undefined && message.task_type === "local_bash" && // Older Claude Code builds omit the flag on foreground calls. message.is_backgrounded !== true && @@ -8064,7 +8062,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( }); const stop = yield* Deferred.make(); const fiber = yield* tailClaudeTaskOutput({ - root: taskOutputRoot, + root: liveQuery.taskOutputRoot, sessionId: message.session_id, taskId: message.task_id, stop, @@ -9365,6 +9363,11 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( (subagent) => subagent.task.status === "running", ), ), + taskOutputRoot: claudeTaskOutputRoot( + adapterOptions.environment, + path, + turnInput.runtimePolicy.cwd, + ), }; yield* Ref.set(queryContext, context); yield* querySession.messages.pipe( From 5d61cf10bc8a4ea42850f27c7c3491e7e5e3529c Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Fri, 23 Oct 2026 10:07:29 -0500 Subject: [PATCH 4/6] fix(claude): keep /tmp for live Bash output on macOS Claude Code's macOS build looks for its task files under CLAUDE_CODE_TMPDIR or /tmp and never reads TMPDIR, which launchd sets on every Mac. Reading TMPDIR there sent the tail to the wrong directory. TMPDIR, TMP and TEMP now apply only off macOS, where Claude Code uses os.tmpdir(). Co-Authored-By: Claude Opus 5.5 --- .../Adapters/ClaudeAdapterV2.test.ts | 4 ++- .../Adapters/ClaudeAdapterV2.testkit.ts | 7 ++++- .../Adapters/ClaudeAdapterV2.ts | 2 ++ .../Adapters/ClaudeTaskOutputTail.test.ts | 29 +++++++++++++------ .../Adapters/ClaudeTaskOutputTail.ts | 19 ++++++------ 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 54cc4973ac365..0429ad3104841 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -6421,7 +6421,8 @@ describe("ClaudeAdapterV2 background wake turns", () => { prefix: "t3-claude-task-output-cwd-", }); // The session opens in /workspace; a relative TMPDIR puts Claude's - // task files under whichever cwd the CLI was launched in. + // task files under whichever cwd the CLI was launched in. Linux, + // because Claude Code's macOS build ignores TMPDIR. const harness = yield* makeWakeHarnessWithOptions({ environment: { TMPDIR: "tmp" } }); const now = yield* DateTime.now; const toolUseId = "toolu_foreground_bash"; @@ -6506,6 +6507,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { ); assert.deepEqual(chunks, ["line 1\n"]); }).pipe( + Effect.provideService(HostProcess.Platform, "linux"), Effect.provide( Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), ), diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts index 542f6019126af..e9ffcf6c85b24 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts @@ -1024,7 +1024,12 @@ const writeReplayTaskOutputFiles = Effect.fn("ClaudeReplay.writeTaskOutputFiles" const stderr = typeof result.stderr === "string" ? result.stderr : ""; outputByToolUseId.set(toolResult.tool_use_id, `${result.stdout}${stderr}`); } - const root = claudeTaskOutputRoot({ CLAUDE_CODE_TMPDIR: tmpRoot }, path, null); + const root = claudeTaskOutputRoot( + { CLAUDE_CODE_TMPDIR: tmpRoot }, + path, + null, + yield* HostProcess.Platform, + ); for (const frame of frames) { if (frame.type !== "system" || frame.subtype !== "task_started") continue; if (frame.task_type !== "local_bash" || frame.is_backgrounded !== false) continue; diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 700117080f5d7..cc0cdf3feb878 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -3920,6 +3920,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( adapterOptions.environment, ).pipe(Effect.provideService(Path.Path, path)); const hostEnvironment = yield* HostProcess.Environment; + const hostPlatform = yield* HostProcess.Platform; const customHome = adapterOptions.settings.homePath.trim().length > 0 || adapterOptions.environment.CLAUDE_CONFIG_DIR !== hostEnvironment.CLAUDE_CONFIG_DIR; @@ -9367,6 +9368,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( adapterOptions.environment, path, turnInput.runtimePolicy.cwd, + hostPlatform, ), }; yield* Ref.set(queryContext, context); diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts index 77a6f79b640de..646bbb054dc2c 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts @@ -109,23 +109,34 @@ describe("tailClaudeTaskOutput", () => { describe("claudeTaskOutputRoot", () => { it.effect.skipIf(process.getuid === undefined)( - "uses the temp dir Claude Code resolves from its environment", + "uses the temp dir Claude Code resolves from its environment on each platform", () => Effect.gen(function* () { const path = yield* Path.Path; const user = `claude-${process.getuid?.()}`; - const root = (environment: Record, cwd: string | null = null) => - claudeTaskOutputRoot(environment, path, cwd); - assert.equal(root({ TMPDIR: "/var/folders/x/T/" }), `/var/folders/x/T/${user}`); - assert.equal(root({ TMP: "/tmp/user/1000" }), `/tmp/user/1000/${user}`); + const root = ( + environment: Record, + platform: NodeJS.Platform, + cwd: string | null = null, + ) => claudeTaskOutputRoot(environment, path, cwd, platform); + assert.equal(root({ TMPDIR: "/var/folders/x/T/" }, "linux"), `/var/folders/x/T/${user}`); + assert.equal(root({ TMP: "/tmp/user/1000" }, "linux"), `/tmp/user/1000/${user}`); + assert.equal(root({}, "linux"), `/tmp/${user}`); + assert.equal(root({ TMPDIR: "/var/folders/x/T/" }, "darwin"), `/tmp/${user}`); assert.equal( - root({ CLAUDE_CODE_TMPDIR: "/srv/claude", TMPDIR: "/var/folders/x/T/" }), + root({ CLAUDE_CODE_TMPDIR: "/srv/claude", TMPDIR: "/var/folders/x/T/" }, "linux"), `/srv/claude/${user}`, ); - assert.equal(root({}), `/tmp/${user}`); - assert.equal(root({ TMPDIR: "tmp" }, "/work/project"), `/work/project/tmp/${user}`); assert.equal( - root({ CLAUDE_CODE_TMPDIR: "../claude" }, "/work/project"), + root({ CLAUDE_CODE_TMPDIR: "/srv/claude", TMPDIR: "/var/folders/x/T/" }, "darwin"), + `/srv/claude/${user}`, + ); + assert.equal( + root({ TMPDIR: "tmp" }, "linux", "/work/project"), + `/work/project/tmp/${user}`, + ); + assert.equal( + root({ CLAUDE_CODE_TMPDIR: "../claude" }, "darwin", "/work/project"), `/work/claude/${user}`, ); }).pipe(Effect.provide(NodeServices.layer)), diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts index 9c8730da8372b..d98fd3b7ea4c1 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts @@ -27,18 +27,19 @@ export function claudeTaskOutputRoot( environment: Readonly> | undefined, path: Path.Path, cwd: string | null, + platform: NodeJS.Platform, ): string | undefined { const uid = process.getuid?.(); if (uid === undefined) return undefined; - // Claude Code falls back to `os.tmpdir()`, which reads these from the - // environment it was launched with: macOS always sets TMPDIR. A relative - // value is relative to Claude's working directory, not this server's. - const base = - environment?.CLAUDE_CODE_TMPDIR?.trim() || - environment?.TMPDIR || - environment?.TMP || - environment?.TEMP || - "/tmp"; + // Claude Code's macOS build falls back to /tmp and ignores the TMPDIR that + // launchd always sets. Its other builds use `os.tmpdir()`, which reads these + // from the environment it was launched with. A relative value is relative + // to Claude's working directory, not this server's. + const fallback = + platform === "darwin" + ? "/tmp" + : environment?.TMPDIR || environment?.TMP || environment?.TEMP || "/tmp"; + const base = environment?.CLAUDE_CODE_TMPDIR?.trim() || fallback; return path.resolve(cwd ?? ".", base, `claude-${uid}`); } From 0893a650059936eaa1210253d80eb67bb9bb153e Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Fri, 30 Oct 2026 01:05:59 +0000 Subject: [PATCH 5/6] fix(claude): resolve task output from physical launch cwd --- .../Adapters/ClaudeAdapterV2.test.ts | 13 +++++++++---- .../orchestration-v2/Adapters/ClaudeAdapterV2.ts | 4 +++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 3a22cc6559ef4..f29b4af85b5ab 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -7972,24 +7972,29 @@ describe("ClaudeAdapterV2 background wake turns", () => { ); it.effect.skipIf(process.getuid === undefined)( - "tails foreground Bash output under the cwd the query launched in, not the session's", + "tails foreground Bash output under the physical cwd the query launched in", () => Effect.scoped( Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const launchCwd = yield* fileSystem.makeTempDirectoryScoped({ + const root = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-task-output-cwd-", }); + const physicalCwd = path.join(root, "real", "project"); + const launchCwd = path.join(root, "link"); + yield* fileSystem.makeDirectory(physicalCwd, { recursive: true }); + yield* fileSystem.symlink(physicalCwd, launchCwd); // The session opens in /workspace; a relative TMPDIR puts Claude's // task files under whichever cwd the CLI was launched in. Linux, // because Claude Code's macOS build ignores TMPDIR. - const harness = yield* makeWakeHarnessWithOptions({ environment: { TMPDIR: "tmp" } }); + const harness = yield* makeWakeHarnessWithOptions({ environment: { TMPDIR: "../tmp" } }); const now = yield* DateTime.now; const toolUseId = "toolu_foreground_bash"; const taskId = "foreground-bash-task"; const tasks = path.join( - launchCwd, + root, + "real", "tmp", `claude-${process.getuid?.()}`, "project", diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 4b06600e1fffe..4f3fc29fa6dba 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -10006,7 +10006,9 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( taskOutputRoot: claudeTaskOutputRoot( adapterOptions.environment, path, - turnInput.runtimePolicy.cwd, + yield* fileSystem + .realPath(turnInput.runtimePolicy.cwd ?? ".") + .pipe(Effect.orElseSucceed(() => turnInput.runtimePolicy.cwd)), hostPlatform, ), }; From 1591816084a66816588ad5ef7e91361905ecb775 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Fri, 30 Oct 2026 01:46:25 +0000 Subject: [PATCH 6/6] fix(claude): resolve task cwd before opening query --- .../src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 4f3fc29fa6dba..008b7ff204f19 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -9954,6 +9954,9 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( ...(appendSystemPromptFileText === undefined ? {} : { appendSystemPromptFileText }), ...(toolSearchOverride === undefined ? {} : { toolSearchOverride }), }); + const resolvedTurnCwd = yield* fileSystem + .realPath(turnInput.runtimePolicy.cwd ?? ".") + .pipe(Effect.orElseSucceed(() => turnInput.runtimePolicy.cwd)); const querySession = yield* queryRunner .open({ threadId: turnInput.threadId, @@ -10006,9 +10009,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( taskOutputRoot: claudeTaskOutputRoot( adapterOptions.environment, path, - yield* fileSystem - .realPath(turnInput.runtimePolicy.cwd ?? ".") - .pipe(Effect.orElseSucceed(() => turnInput.runtimePolicy.cwd)), + resolvedTurnCwd, hostPlatform, ), };