From 47cc26e85ec55130c4c311f799ea7d0130408f60 Mon Sep 17 00:00:00 2001 From: sidreyas Date: Sat, 7 Nov 2026 00:30:35 +0530 Subject: [PATCH 1/3] fix(server): resolve Claude WinGet launchers on Windows --- .../provider/Drivers/ClaudeExecutable.test.ts | 91 +++++++++++++++++++ .../src/provider/Drivers/ClaudeExecutable.ts | 40 +++++++- 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts b/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts index e1319caf69b16..1c65c76341d80 100644 --- a/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts +++ b/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts @@ -1,14 +1,25 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Mocks the synchronous Windows resolver's filesystem boundary. +import * as NodeFS from "node:fs"; + import { describe, expect, it } from "@effect/vitest"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import * as Effect from "effect/Effect"; +import { vi } from "vite-plus/test"; import { ClaudeExecutableFileCheck, resolveClaudeSdkExecutablePath } from "./ClaudeExecutable.ts"; +vi.mock("node:fs", { spy: true }); + const NPM_DIR = "C:\\Users\\dev\\AppData\\Roaming\\npm"; const NPM_SHIM = `${NPM_DIR}\\claude.cmd`; const NPM_PACKAGE_EXE = `${NPM_DIR}\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe`; const NPM_PACKAGE_CLI = `${NPM_DIR}\\node_modules\\@anthropic-ai\\claude-code\\cli.js`; +const LOCAL_APP_DATA = "C:\\Users\\Developer Name\\AppData\\Local"; +const WINGET_SHIM = `${LOCAL_APP_DATA}\\Microsoft\\WinGet\\Links\\claude.cmd`; +const WINGET_PACKAGE = "Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe"; +const WINGET_EXE = `${LOCAL_APP_DATA}\\Microsoft\\WinGet\\Packages\\${WINGET_PACKAGE}\\claude.exe`; +const WINGET_LAUNCHER = `@echo off\r\n"%LOCALAPPDATA%\\Microsoft\\WinGet\\Packages\\${WINGET_PACKAGE}\\claude.exe" %*\r\n`; function withWindowsResolution(input: { readonly resolvedCommand: string | undefined; @@ -24,6 +35,78 @@ function withWindowsResolution(input: { } describe("resolveClaudeSdkExecutablePath", () => { + it.effect("follows the WinGet launcher to its native executable", () => + Effect.gen(function* () { + for (const [launcher, localAppData] of [ + [WINGET_LAUNCHER, LOCAL_APP_DATA], + [WINGET_LAUNCHER.toLowerCase().replaceAll("\r\n", "\n"), LOCAL_APP_DATA], + [WINGET_LAUNCHER, "C:\\Users\\literal $& %name%\\AppData\\Local"], + ] as const) { + using _read = vi.spyOn(NodeFS, "readFileSync").mockReturnValue(launcher); + const shim = WINGET_SHIM.replace(LOCAL_APP_DATA, () => localAppData); + const executable = WINGET_EXE.replace(LOCAL_APP_DATA, () => localAppData); + for (const environment of [ + { LOCALAPPDATA: localAppData }, + { LocalAppData: localAppData }, + ]) { + for (const binaryPath of ["claude", shim]) { + expect( + yield* resolveClaudeSdkExecutablePath(binaryPath, environment).pipe( + withWindowsResolution({ + resolvedCommand: shim, + existingFiles: [executable], + }), + ), + ).toBe(executable); + } + } + } + }), + ); + + it.effect("leaves unsupported or missing WinGet targets unresolved", () => + Effect.gen(function* () { + for (const launcher of [ + WINGET_LAUNCHER.replace(WINGET_PACKAGE, "Other.Package"), + WINGET_LAUNCHER.replace("%LOCALAPPDATA%", "%APPDATA%"), + WINGET_LAUNCHER.replace("claude.exe", "..\\other.exe"), + WINGET_LAUNCHER.replace(" %*", " --extra %*"), + `${WINGET_LAUNCHER}echo side effect\r\n`, + WINGET_LAUNCHER.replace(" %*", " %* & echo side effect"), + ]) { + using _read = vi.spyOn(NodeFS, "readFileSync").mockReturnValue(launcher); + expect( + yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe( + withWindowsResolution({ resolvedCommand: WINGET_SHIM, existingFiles: [WINGET_EXE] }), + ), + ).toBe("claude"); + } + using _read = vi.spyOn(NodeFS, "readFileSync").mockReturnValue(WINGET_LAUNCHER); + for (const environment of [{}, { LOCALAPPDATA: "relative" }]) { + expect( + yield* resolveClaudeSdkExecutablePath("claude", environment).pipe( + withWindowsResolution({ resolvedCommand: WINGET_SHIM, existingFiles: [WINGET_EXE] }), + ), + ).toBe("claude"); + } + expect( + yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe( + withWindowsResolution({ resolvedCommand: WINGET_SHIM }), + ), + ).toBe("claude"); + for (const shim of [NPM_SHIM, WINGET_SHIM.replace(".cmd", ".ps1")]) { + expect( + yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe( + withWindowsResolution({ + resolvedCommand: shim, + existingFiles: [WINGET_EXE], + }), + ), + ).toBe("claude"); + } + }), + ); + it.effect("follows an npm launcher shim to the packaged native binary", () => Effect.gen(function* () { expect( @@ -85,6 +168,14 @@ describe("resolveClaudeSdkExecutablePath", () => { withWindowsResolution({ resolvedCommand: NPM_SHIM }), ), ).toBe("claude"); + using _read = vi.spyOn(NodeFS, "readFileSync").mockImplementation(() => { + throw new Error("unreadable launcher"); + }); + expect( + yield* resolveClaudeSdkExecutablePath(WINGET_SHIM, { LOCALAPPDATA: LOCAL_APP_DATA }).pipe( + withWindowsResolution({ resolvedCommand: WINGET_SHIM, existingFiles: [WINGET_EXE] }), + ), + ).toBe(WINGET_SHIM); }), ); }); diff --git a/apps/server/src/provider/Drivers/ClaudeExecutable.ts b/apps/server/src/provider/Drivers/ClaudeExecutable.ts index 5e4cbb4db6efe..58c133e582917 100644 --- a/apps/server/src/provider/Drivers/ClaudeExecutable.ts +++ b/apps/server/src/provider/Drivers/ClaudeExecutable.ts @@ -19,6 +19,39 @@ const NPM_PACKAGE_ENTRY_CANDIDATES = [ ["node_modules", "@anthropic-ai", "claude-code", "cli.js"], ] as const; +const WINGET_PACKAGE_ENTRY = [ + "Microsoft", + "WinGet", + "Packages", + "Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe", + "claude.exe", +] as const; + +function resolveWinGetExecutablePath(shimPath: string, environment: NodeJS.ProcessEnv) { + const localAppData = Object.entries(environment).find( + ([key]) => key.toUpperCase() === "LOCALAPPDATA", + )?.[1]; + if (!localAppData || !NodePath.win32.isAbsolute(localAppData)) return undefined; + const launcherPath = NodePath.win32.join( + localAppData, + "Microsoft", + "WinGet", + "Links", + "claude.cmd", + ); + if (NodePath.win32.normalize(shimPath).toLowerCase() !== launcherPath.toLowerCase()) + return undefined; + + try { + const launcher = NodeFS.readFileSync(shimPath, "utf8").replaceAll("\r\n", "\n").trim(); + const expectedLauncher = `@echo off\n"%LOCALAPPDATA%\\${WINGET_PACKAGE_ENTRY.join("\\")}" %*`; + if (launcher.toLowerCase() !== expectedLauncher.toLowerCase()) return undefined; + return NodePath.win32.join(localAppData, ...WINGET_PACKAGE_ENTRY); + } catch { + return undefined; + } +} + export type ExecutableFileCheck = (filePath: string) => boolean; function isExistingFile(filePath: string): boolean { @@ -60,8 +93,13 @@ export const resolveClaudeSdkExecutablePath = Effect.fn("resolveClaudeSdkExecuta } } + const winGetExecutable = resolveWinGetExecutablePath(resolved, environment); + if (winGetExecutable && isFile(winGetExecutable)) { + return winGetExecutable; + } + yield* Effect.logWarning( - "Claude launcher shim resolved but no known package entry was found next to it; the Claude Agent SDK cannot spawn launcher scripts directly.", + "Claude launcher shim resolved but no known package entry was found; the Claude Agent SDK cannot spawn launcher scripts directly.", { binaryPath, resolvedShimPath: resolved }, ); return binaryPath; From 6957bcb198b07f2267841877ce7ccb841d8fa060 Mon Sep 17 00:00:00 2001 From: sidreyas Date: Sat, 7 Nov 2026 01:23:59 +0530 Subject: [PATCH 2/3] fix(server): resolve equivalent WinGet launcher paths --- .../provider/Drivers/ClaudeExecutable.test.ts | 47 +++++++++++++++---- .../src/provider/Drivers/ClaudeExecutable.ts | 9 ++-- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts b/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts index 1c65c76341d80..5c269334b6c4e 100644 --- a/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts +++ b/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts @@ -49,15 +49,24 @@ describe("resolveClaudeSdkExecutablePath", () => { { LOCALAPPDATA: localAppData }, { LocalAppData: localAppData }, ]) { - for (const binaryPath of ["claude", shim]) { - expect( - yield* resolveClaudeSdkExecutablePath(binaryPath, environment).pipe( - withWindowsResolution({ - resolvedCommand: shim, - existingFiles: [executable], - }), - ), - ).toBe(executable); + for (const resolvedCommand of [ + shim, + "C:\\Users\\DEVELO~1\\AppData\\Local\\Microsoft\\WinGet\\Links\\claude.cmd", + "C:\\winget-links\\claude.cmd", + ]) { + using _realpath = vi + .spyOn(NodeFS.realpathSync, "native") + .mockImplementation((filePath) => { + if (filePath === shim || filePath === resolvedCommand) return shim; + throw new Error("unknown launcher"); + }); + for (const binaryPath of ["claude", resolvedCommand]) { + expect( + yield* resolveClaudeSdkExecutablePath(binaryPath, environment).pipe( + withWindowsResolution({ resolvedCommand, existingFiles: [executable] }), + ), + ).toBe(executable); + } } } } @@ -94,7 +103,14 @@ describe("resolveClaudeSdkExecutablePath", () => { withWindowsResolution({ resolvedCommand: WINGET_SHIM }), ), ).toBe("claude"); - for (const shim of [NPM_SHIM, WINGET_SHIM.replace(".cmd", ".ps1")]) { + using _realpath = vi + .spyOn(NodeFS.realpathSync, "native") + .mockImplementation((filePath) => String(filePath)); + for (const shim of [ + NPM_SHIM, + WINGET_SHIM.replace(".cmd", ".ps1"), + "C:\\different-winget-links\\claude.cmd", + ]) { expect( yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe( withWindowsResolution({ @@ -176,6 +192,17 @@ describe("resolveClaudeSdkExecutablePath", () => { withWindowsResolution({ resolvedCommand: WINGET_SHIM, existingFiles: [WINGET_EXE] }), ), ).toBe(WINGET_SHIM); + using _realpath = vi.spyOn(NodeFS.realpathSync, "native").mockImplementation(() => { + throw new Error("unreadable path"); + }); + expect( + yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe( + withWindowsResolution({ + resolvedCommand: "C:\\unreadable-winget-links\\claude.cmd", + existingFiles: [WINGET_EXE], + }), + ), + ).toBe("claude"); }), ); }); diff --git a/apps/server/src/provider/Drivers/ClaudeExecutable.ts b/apps/server/src/provider/Drivers/ClaudeExecutable.ts index 58c133e582917..94892e5ed78ee 100644 --- a/apps/server/src/provider/Drivers/ClaudeExecutable.ts +++ b/apps/server/src/provider/Drivers/ClaudeExecutable.ts @@ -39,10 +39,13 @@ function resolveWinGetExecutablePath(shimPath: string, environment: NodeJS.Proce "Links", "claude.cmd", ); - if (NodePath.win32.normalize(shimPath).toLowerCase() !== launcherPath.toLowerCase()) - return undefined; - try { + if ( + NodePath.win32.normalize(shimPath).toLowerCase() !== launcherPath.toLowerCase() && + NodeFS.realpathSync.native(shimPath).toLowerCase() !== + NodeFS.realpathSync.native(launcherPath).toLowerCase() + ) + return undefined; const launcher = NodeFS.readFileSync(shimPath, "utf8").replaceAll("\r\n", "\n").trim(); const expectedLauncher = `@echo off\n"%LOCALAPPDATA%\\${WINGET_PACKAGE_ENTRY.join("\\")}" %*`; if (launcher.toLowerCase() !== expectedLauncher.toLowerCase()) return undefined; From a9bde70046524564a72af8673e65dd0fcc57beea Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 15 Nov 2026 04:15:07 +0000 Subject: [PATCH 3/3] refactor(server): find the WinGet Claude executable beside its launcher WinGet's Links\claude.cmd always forwards to ..\Packages\\claude.exe, so check that path as one more sibling candidate next to the npm entries instead of reading LOCALAPPDATA, canonicalizing paths, and parsing the launcher text. This drops the node:fs mocks from the tests and keeps one regression case. Co-Authored-By: Claude Opus 5.5 --- .../provider/Drivers/ClaudeExecutable.test.ts | 131 ++---------------- .../src/provider/Drivers/ClaudeExecutable.ts | 53 ++----- 2 files changed, 21 insertions(+), 163 deletions(-) diff --git a/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts b/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts index 5c269334b6c4e..b6dc61470e497 100644 --- a/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts +++ b/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts @@ -1,25 +1,17 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Mocks the synchronous Windows resolver's filesystem boundary. -import * as NodeFS from "node:fs"; - import { describe, expect, it } from "@effect/vitest"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import * as Effect from "effect/Effect"; -import { vi } from "vite-plus/test"; import { ClaudeExecutableFileCheck, resolveClaudeSdkExecutablePath } from "./ClaudeExecutable.ts"; -vi.mock("node:fs", { spy: true }); - const NPM_DIR = "C:\\Users\\dev\\AppData\\Roaming\\npm"; const NPM_SHIM = `${NPM_DIR}\\claude.cmd`; const NPM_PACKAGE_EXE = `${NPM_DIR}\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe`; const NPM_PACKAGE_CLI = `${NPM_DIR}\\node_modules\\@anthropic-ai\\claude-code\\cli.js`; -const LOCAL_APP_DATA = "C:\\Users\\Developer Name\\AppData\\Local"; -const WINGET_SHIM = `${LOCAL_APP_DATA}\\Microsoft\\WinGet\\Links\\claude.cmd`; -const WINGET_PACKAGE = "Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe"; -const WINGET_EXE = `${LOCAL_APP_DATA}\\Microsoft\\WinGet\\Packages\\${WINGET_PACKAGE}\\claude.exe`; -const WINGET_LAUNCHER = `@echo off\r\n"%LOCALAPPDATA%\\Microsoft\\WinGet\\Packages\\${WINGET_PACKAGE}\\claude.exe" %*\r\n`; +const WINGET_DIR = "C:\\Users\\dev\\AppData\\Local\\Microsoft\\WinGet"; +const WINGET_SHIM = `${WINGET_DIR}\\Links\\claude.cmd`; +const WINGET_EXE = `${WINGET_DIR}\\Packages\\Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe\\claude.exe`; function withWindowsResolution(input: { readonly resolvedCommand: string | undefined; @@ -35,94 +27,6 @@ function withWindowsResolution(input: { } describe("resolveClaudeSdkExecutablePath", () => { - it.effect("follows the WinGet launcher to its native executable", () => - Effect.gen(function* () { - for (const [launcher, localAppData] of [ - [WINGET_LAUNCHER, LOCAL_APP_DATA], - [WINGET_LAUNCHER.toLowerCase().replaceAll("\r\n", "\n"), LOCAL_APP_DATA], - [WINGET_LAUNCHER, "C:\\Users\\literal $& %name%\\AppData\\Local"], - ] as const) { - using _read = vi.spyOn(NodeFS, "readFileSync").mockReturnValue(launcher); - const shim = WINGET_SHIM.replace(LOCAL_APP_DATA, () => localAppData); - const executable = WINGET_EXE.replace(LOCAL_APP_DATA, () => localAppData); - for (const environment of [ - { LOCALAPPDATA: localAppData }, - { LocalAppData: localAppData }, - ]) { - for (const resolvedCommand of [ - shim, - "C:\\Users\\DEVELO~1\\AppData\\Local\\Microsoft\\WinGet\\Links\\claude.cmd", - "C:\\winget-links\\claude.cmd", - ]) { - using _realpath = vi - .spyOn(NodeFS.realpathSync, "native") - .mockImplementation((filePath) => { - if (filePath === shim || filePath === resolvedCommand) return shim; - throw new Error("unknown launcher"); - }); - for (const binaryPath of ["claude", resolvedCommand]) { - expect( - yield* resolveClaudeSdkExecutablePath(binaryPath, environment).pipe( - withWindowsResolution({ resolvedCommand, existingFiles: [executable] }), - ), - ).toBe(executable); - } - } - } - } - }), - ); - - it.effect("leaves unsupported or missing WinGet targets unresolved", () => - Effect.gen(function* () { - for (const launcher of [ - WINGET_LAUNCHER.replace(WINGET_PACKAGE, "Other.Package"), - WINGET_LAUNCHER.replace("%LOCALAPPDATA%", "%APPDATA%"), - WINGET_LAUNCHER.replace("claude.exe", "..\\other.exe"), - WINGET_LAUNCHER.replace(" %*", " --extra %*"), - `${WINGET_LAUNCHER}echo side effect\r\n`, - WINGET_LAUNCHER.replace(" %*", " %* & echo side effect"), - ]) { - using _read = vi.spyOn(NodeFS, "readFileSync").mockReturnValue(launcher); - expect( - yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe( - withWindowsResolution({ resolvedCommand: WINGET_SHIM, existingFiles: [WINGET_EXE] }), - ), - ).toBe("claude"); - } - using _read = vi.spyOn(NodeFS, "readFileSync").mockReturnValue(WINGET_LAUNCHER); - for (const environment of [{}, { LOCALAPPDATA: "relative" }]) { - expect( - yield* resolveClaudeSdkExecutablePath("claude", environment).pipe( - withWindowsResolution({ resolvedCommand: WINGET_SHIM, existingFiles: [WINGET_EXE] }), - ), - ).toBe("claude"); - } - expect( - yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe( - withWindowsResolution({ resolvedCommand: WINGET_SHIM }), - ), - ).toBe("claude"); - using _realpath = vi - .spyOn(NodeFS.realpathSync, "native") - .mockImplementation((filePath) => String(filePath)); - for (const shim of [ - NPM_SHIM, - WINGET_SHIM.replace(".cmd", ".ps1"), - "C:\\different-winget-links\\claude.cmd", - ]) { - expect( - yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe( - withWindowsResolution({ - resolvedCommand: shim, - existingFiles: [WINGET_EXE], - }), - ), - ).toBe("claude"); - } - }), - ); - it.effect("follows an npm launcher shim to the packaged native binary", () => Effect.gen(function* () { expect( @@ -136,6 +40,16 @@ describe("resolveClaudeSdkExecutablePath", () => { }), ); + it.effect("follows the WinGet launcher to the package's native binary", () => + Effect.gen(function* () { + expect( + yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( + withWindowsResolution({ resolvedCommand: WINGET_SHIM, existingFiles: [WINGET_EXE] }), + ), + ).toBe(WINGET_EXE); + }), + ); + it.effect("follows .bat and .ps1 launcher shims the same way", () => Effect.gen(function* () { for (const shim of [`${NPM_DIR}\\claude.bat`, `${NPM_DIR}\\claude.ps1`]) { @@ -184,25 +98,6 @@ describe("resolveClaudeSdkExecutablePath", () => { withWindowsResolution({ resolvedCommand: NPM_SHIM }), ), ).toBe("claude"); - using _read = vi.spyOn(NodeFS, "readFileSync").mockImplementation(() => { - throw new Error("unreadable launcher"); - }); - expect( - yield* resolveClaudeSdkExecutablePath(WINGET_SHIM, { LOCALAPPDATA: LOCAL_APP_DATA }).pipe( - withWindowsResolution({ resolvedCommand: WINGET_SHIM, existingFiles: [WINGET_EXE] }), - ), - ).toBe(WINGET_SHIM); - using _realpath = vi.spyOn(NodeFS.realpathSync, "native").mockImplementation(() => { - throw new Error("unreadable path"); - }); - expect( - yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe( - withWindowsResolution({ - resolvedCommand: "C:\\unreadable-winget-links\\claude.cmd", - existingFiles: [WINGET_EXE], - }), - ), - ).toBe("claude"); }), ); }); diff --git a/apps/server/src/provider/Drivers/ClaudeExecutable.ts b/apps/server/src/provider/Drivers/ClaudeExecutable.ts index 94892e5ed78ee..5797066446b99 100644 --- a/apps/server/src/provider/Drivers/ClaudeExecutable.ts +++ b/apps/server/src/provider/Drivers/ClaudeExecutable.ts @@ -13,48 +13,16 @@ const WINDOWS_SHIM_EXTENSIONS: ReadonlySet = new Set([".cmd", ".bat", ". export const isWindowsClaudeLauncherShimPath = (filePath: string): boolean => WINDOWS_SHIM_EXTENSIONS.has(NodePath.win32.extname(filePath).toLowerCase()); -/** New packages provide bin/claude.exe; older packages need a JavaScript runtime for cli.js. */ -const NPM_PACKAGE_ENTRY_CANDIDATES = [ +/** + * npm: new packages provide bin/claude.exe; older ones need a JavaScript runtime for cli.js. + * WinGet: Links\claude.cmd forwards to the package's native executable under ..\Packages. + */ +const PACKAGE_ENTRY_CANDIDATES = [ ["node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"], ["node_modules", "@anthropic-ai", "claude-code", "cli.js"], + ["..", "Packages", "Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe", "claude.exe"], ] as const; -const WINGET_PACKAGE_ENTRY = [ - "Microsoft", - "WinGet", - "Packages", - "Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe", - "claude.exe", -] as const; - -function resolveWinGetExecutablePath(shimPath: string, environment: NodeJS.ProcessEnv) { - const localAppData = Object.entries(environment).find( - ([key]) => key.toUpperCase() === "LOCALAPPDATA", - )?.[1]; - if (!localAppData || !NodePath.win32.isAbsolute(localAppData)) return undefined; - const launcherPath = NodePath.win32.join( - localAppData, - "Microsoft", - "WinGet", - "Links", - "claude.cmd", - ); - try { - if ( - NodePath.win32.normalize(shimPath).toLowerCase() !== launcherPath.toLowerCase() && - NodeFS.realpathSync.native(shimPath).toLowerCase() !== - NodeFS.realpathSync.native(launcherPath).toLowerCase() - ) - return undefined; - const launcher = NodeFS.readFileSync(shimPath, "utf8").replaceAll("\r\n", "\n").trim(); - const expectedLauncher = `@echo off\n"%LOCALAPPDATA%\\${WINGET_PACKAGE_ENTRY.join("\\")}" %*`; - if (launcher.toLowerCase() !== expectedLauncher.toLowerCase()) return undefined; - return NodePath.win32.join(localAppData, ...WINGET_PACKAGE_ENTRY); - } catch { - return undefined; - } -} - export type ExecutableFileCheck = (filePath: string) => boolean; function isExistingFile(filePath: string): boolean { @@ -89,20 +57,15 @@ export const resolveClaudeSdkExecutablePath = Effect.fn("resolveClaudeSdkExecuta } const shimDirectory = NodePath.win32.dirname(resolved); - for (const entrySegments of NPM_PACKAGE_ENTRY_CANDIDATES) { + for (const entrySegments of PACKAGE_ENTRY_CANDIDATES) { const candidate = NodePath.win32.join(shimDirectory, ...entrySegments); if (isFile(candidate)) { return candidate; } } - const winGetExecutable = resolveWinGetExecutablePath(resolved, environment); - if (winGetExecutable && isFile(winGetExecutable)) { - return winGetExecutable; - } - yield* Effect.logWarning( - "Claude launcher shim resolved but no known package entry was found; the Claude Agent SDK cannot spawn launcher scripts directly.", + "Claude launcher shim resolved but no known package entry was found next to it; the Claude Agent SDK cannot spawn launcher scripts directly.", { binaryPath, resolvedShimPath: resolved }, ); return binaryPath;