From 5ecb8f60b853b8a1a65dd6e3fdde7106dc074ad2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 28 Oct 2026 20:41:33 +0000 Subject: [PATCH 1/2] test(server): reproduce #26631 ~/ Claude launch argument paths These tests reproduce #26631: --settings and --append-system-prompt-file values starting with ~/ resolve under the project cwd instead of the home directory, so the turn fails before Claude opens. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/ClaudeAdapterV2.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index ba4b7444b7b08..166742448bf8d 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -1060,6 +1060,45 @@ describe("ClaudeAdapterV2 append-system-prompt-file launch argument", () => { ), ); + // regression: #26631 `~/` launch argument paths resolved under the project cwd + it.effect.each([ + [ + "--settings ~/.claude/settings.json", + (options: ClaudeAdapterV2.ClaudeAgentSdkQueryOptions) => + assert.deepInclude(options.settings, { skillOverrides: { lint: "off" } }), + ], + [ + "--append-system-prompt-file ~/.claude/settings.json", + (options: ClaudeAdapterV2.ClaudeAgentSdkQueryOptions) => + assert.include(appendSystemPrompt(options), '{"skillOverrides":{"lint":"off"}}'), + ], + ] as const)("reads %s from the home directory", ([launchArgs, assertOptions]) => + Effect.scoped( + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-home-" }); + const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-home-cwd-" }); + yield* fileSystem.makeDirectory(NodePath.join(home, ".claude")); + yield* fileSystem.writeFileString( + NodePath.join(home, ".claude", "settings.json"), + '{"skillOverrides":{"lint":"off"}}', + ); + const { exit, opened } = yield* startTurnWithLaunchArgs(launchArgs, cwd).pipe( + Effect.provideService(HostProcess.HomeDirectory, home), + ); + if (Exit.isFailure(exit)) { + assert.fail(makeProviderFailure({ cause: exit.cause, class: "provider_error" }).message); + } + assert.lengthOf(opened, 1); + assertOptions(opened[0]!); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + // regression: #20413 malformed append-system-prompt launch args fail before a query opens it.effect.each([ [ From 343cb3b604659ff3737b088e92650e1cb26b9215 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 28 Oct 2026 20:46:40 +0000 Subject: [PATCH 2/2] fix(server): Claude launch-arg file paths expand ~ `--settings ~/.claude/settings.json` and `--append-system-prompt-file ~/...` were resolved against the project cwd, so they read `/~/...`. Expand a leading `~` with the existing expandHomePath and HostProcess.HomeDirectory before resolving, matching binaryPath. Relative paths still resolve against the cwd; absolute paths and inline JSON are unchanged. Fixes pingdotgg/t3code#26631 Co-Authored-By: Claude Opus 5.5 --- .../Adapters/ClaudeAdapterV2.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 030fb5785e6d9..5ba36027defc3 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1599,9 +1599,9 @@ export function makeClaudeQueryOptions(input: { /** * Checks the append launch arguments the way the Claude CLI would and reads - * the file relative to the process cwd. makeClaudeQueryOptions folds these - * flags out of extraArgs, so a value the CLI would have rejected has to be - * rejected here or it silently disappears. + * the file relative to the process cwd, expanding a leading ~. + * makeClaudeQueryOptions folds these flags out of extraArgs, so a value the CLI + * would have rejected has to be rejected here or it silently disappears. */ const readClaudeAppendSystemPromptFile = Effect.fnUntraced(function* (input: { readonly settings: ClaudeSettings | undefined; @@ -1626,7 +1626,10 @@ const readClaudeAppendSystemPromptFile = Effect.fnUntraced(function* (input: { flag: "--append-system-prompt-file", }); } - const filePath = input.path.resolve(input.cwd ?? ".", file); + const filePath = input.path.resolve( + input.cwd ?? ".", + expandHomePath(file, yield* HostProcess.HomeDirectory), + ); return yield* input.fileSystem .readFileString(filePath) .pipe( @@ -1640,8 +1643,8 @@ const decodeClaudeLaunchArgSettings = Schema.decodeUnknownEffect( /** * Reads the --settings launch argument: a value starting with a brace is inline - * JSON, anything else is a file path relative to the cwd. makeClaudeQueryOptions - * merges the result beneath T3's own settings. + * JSON, anything else is a file path relative to the cwd, expanding a leading ~. + * makeClaudeQueryOptions merges the result beneath T3's own settings. */ const readClaudeLaunchArgSettings = Effect.fn("ClaudeAdapterV2.readLaunchArgSettings")( function* (input: { @@ -1662,7 +1665,10 @@ const readClaudeLaunchArgSettings = Effect.fn("ClaudeAdapterV2.readLaunchArgSett Effect.mapError((cause) => new ClaudeSettingsLaunchArgError({ cause })), )) as ClaudeSdkSettings; } - const filePath = input.path.resolve(input.cwd ?? ".", trimmed); + const filePath = input.path.resolve( + input.cwd ?? ".", + expandHomePath(trimmed, yield* HostProcess.HomeDirectory), + ); return (yield* input.fileSystem.readFileString(filePath).pipe( Effect.flatMap(decodeClaudeLaunchArgSettings), Effect.mapError((cause) => new ClaudeSettingsLaunchArgError({ path: filePath, cause })),