fix(wsl): open WSL paths in Cursor's WSL mode; ignore Windows-side CLIs in WSL hook setup (#27681)

* fix(editor): open WSL worktrees in Cursor as WSL remote windows

Cursor received the raw WSL UNC path and opened it as a local Windows folder. Its CLI accepts --remote wsl+<distro> like VS Code, so route WSL UNC paths through the same remote args while keeping --new-window.

* fix(wsl): ignore Windows-mounted CLIs in guest hook agent detection

WSL appends the Windows PATH, so the guest relay's agent lookup reported a Windows-only npm shim (e.g. /mnt/c/.../npm/codex) as installed in the distro and installed hooks for it. Reuse the Windows-mount skip that host-side WSL detection already applies; fish shells now hand their login PATH to the same POSIX walk.
This commit is contained in:
Neil
2026-10-11 18:43:09 -07:00
committed by GitHub
parent 83056e5edb
commit d74d069990
4 changed files with 85 additions and 49 deletions
+35 -5
View File
@@ -405,14 +405,44 @@ describe('resolveExternalEditorLaunchSpec', () => {
}
)
it.each([
['C:\\Tools\\cursor.exe', '\\\\wsl.localhost\\Ubuntu\\home\\ada\\project', 'wsl+Ubuntu'],
[
'C:\\Users\\me\\AppData\\Local\\Programs\\cursor\\resources\\app\\bin\\cursor.cmd',
'\\\\wsl$\\Debian\\home\\ada\\project',
'wsl+Debian'
]
])('opens WSL UNC workspaces with %s in a new WSL remote window', (command, pathValue, auth) => {
expect(resolveExternalEditorLaunchSpec(command, pathValue, { platform: 'win32' })).toEqual({
kind: 'executable',
hideWindowsConsole: true,
spawnCmd: command,
spawnArgs: ['--new-window', '--remote', auth, '/home/ada/project']
})
})
it.each(['darwin', 'linux'] as const)(
'keeps WSL-looking Cursor paths local on %s',
(platform) => {
const pathValue = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\project'
expect(resolveExternalEditorLaunchSpec('cursor', pathValue, { platform }).spawnArgs).toEqual([
'--new-window',
pathValue
])
}
)
it('keeps non-WSL Windows paths local for Cursor', () => {
expect(
resolveExternalEditorLaunchSpec('C:\\Tools\\cursor.exe', '\\\\server\\share\\project', {
platform: 'win32'
}).spawnArgs
).toEqual(['--new-window', '\\\\server\\share\\project'])
})
it('does not add VS Code remote arguments to other editors', () => {
const pathValue = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\project'
expect(
resolveExternalEditorLaunchSpec('C:\\Tools\\cursor.exe', pathValue, {
platform: 'win32'
}).spawnArgs
).toEqual(['--new-window', pathValue])
expect(
resolveExternalEditorLaunchSpec('C:\\Tools\\codium.exe', pathValue, {
platform: 'win32'
+18 -13
View File
@@ -98,20 +98,25 @@ function buildExecutableArgs(
pathValue: string,
platform: NodeJS.Platform
): string[] {
const launcherBaseName = getLauncherBaseName(editorCommand)
if (launcherBaseName === 'cursor') {
// Why: Cursor can route bare folder launches through the last active
// workbench. A new window keeps "Open in Cursor" scoped to this worktree.
return ['--new-window', pathValue]
const isCursor = getLauncherBaseName(editorCommand) === 'cursor'
const targetArgs = buildWslRemoteTargetArgs(editorCommand, pathValue, platform, isCursor)
// Why: Cursor can route bare folder launches through the last active
// workbench. A new window keeps "Open in Cursor" scoped to this worktree.
return isCursor ? ['--new-window', ...targetArgs] : targetArgs
}
function buildWslRemoteTargetArgs(
editorCommand: string,
pathValue: string,
platform: NodeJS.Platform,
isCursor: boolean
): string[] {
if (platform !== 'win32' || !(isCursor || isVsCodeLauncherExecutable(editorCommand))) {
return [pathValue]
}
if (platform === 'win32' && isVsCodeLauncherExecutable(editorCommand)) {
const wslPath = parseWslUncPath(pathValue)
if (wslPath) {
// Why: VS Code otherwise treats a WSL UNC path as a local Windows folder.
return ['--remote', `wsl+${wslPath.distro}`, wslPath.linuxPath]
}
}
return [pathValue]
const wslPath = parseWslUncPath(pathValue)
// Why: both editors otherwise treat a WSL UNC path as a local Windows folder.
return wslPath ? ['--remote', `wsl+${wslPath.distro}`, wslPath.linuxPath] : [pathValue]
}
function isCompoundShellCommand(command: string): boolean {
+21 -21
View File
@@ -40,28 +40,20 @@ import {
PreflightHandler
} from './preflight-handler'
function lookupArgs(command: string, mode: '-lc' | '-ilc' = '-lc'): string[] {
function shLookupScript(command: string): string {
return [
mode,
[
buildPosixCommandPathLookupScript({ kind: 'literal', value: command }),
'if [ -n "$resolved" ]; then',
'printf \'__ORCA_AGENT_PATH__%s\\n\' "$resolved"',
'fi'
].join('\n')
]
buildPosixCommandPathLookupScript(
{ kind: 'literal', value: command },
{ skipWindowsMountDirs: true }
),
'if [ -n "$resolved" ]; then',
'printf \'__ORCA_AGENT_PATH__%s\\n\' "$resolved"',
'fi'
].join('\n')
}
function fishLookupArgs(command: string): string[] {
return [
'-ilc',
[
`set -l resolved (command -v ${command} 2>/dev/null)`,
'if test -n "$resolved"',
'printf \'__ORCA_AGENT_PATH__%s\\n\' "$resolved"',
'end'
].join('\n')
]
function lookupArgs(command: string, mode: '-lc' | '-ilc' = '-lc'): string[] {
return [mode, shLookupScript(command)]
}
beforeEach(() => {
@@ -102,10 +94,18 @@ describe('buildCommandLookupSpecs', () => {
})
})
it('uses fish syntax for trusted fish shells', () => {
it('skips Windows-mounted PATH dirs so a Windows-only shim is not a guest install', () => {
const [shellSpec] = buildCommandLookupSpecs('codex', 'linux', { SHELL: '/bin/bash' }, null)
expect(shellSpec.args[1]).toContain('/proc/mounts')
expect(shellSpec.args[1]).toBe(shLookupScript('codex'))
})
it('runs the POSIX lookup under the fish login PATH for trusted fish shells', () => {
const script = shLookupScript('codex')
const fishQuoted = `'${script.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
expect(buildCommandLookupSpecs('codex', 'linux', { SHELL: '/usr/bin/fish' }, null)[0]).toEqual({
file: '/usr/bin/fish',
args: fishLookupArgs("'codex'")
args: ['-ilc', `exec /bin/sh -c ${fishQuoted}`]
})
})
+11 -10
View File
@@ -268,7 +268,12 @@ function buildPosixCommandLookupSpec(command: string, shell: string): CommandLoo
function buildShCommandLookupScript(command: string): string {
// Why: login shells may define aliases or functions that mask the PATH executable.
return [
buildPosixCommandPathLookupScript({ kind: 'literal', value: command }),
// Why: WSL appends the Windows PATH, so a Windows-only npm shim would
// otherwise read as a guest install; same skip as host-side WSL detection.
buildPosixCommandPathLookupScript(
{ kind: 'literal', value: command },
{ skipWindowsMountDirs: true }
),
'if [ -n "$resolved" ]; then',
`printf '${AGENT_PATH_PREFIX}%s\\n' "$resolved"`,
'fi'
@@ -276,13 +281,8 @@ function buildShCommandLookupScript(command: string): string {
}
function buildFishCommandLookupScript(command: string): string {
const quotedCommand = shellQuote(command)
return [
`set -l resolved (command -v ${quotedCommand} 2>/dev/null)`,
'if test -n "$resolved"',
`printf '${AGENT_PATH_PREFIX}%s\\n' "$resolved"`,
'end'
].join('\n')
// Why: fish only supplies its login PATH; the POSIX walk owns the Windows-mount skip.
return `exec /bin/sh -c ${fishQuote(buildShCommandLookupScript(command))}`
}
function resolveAccountLoginShell(
@@ -327,6 +327,7 @@ function getShellCommandMode(shell: string): '-lc' | '-ilc' {
return shellName === 'sh' || shellName === 'dash' ? '-lc' : '-ilc'
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`
// Fish single quotes treat only `\\` and `\'` as escapes.
function fishQuote(value: string): string {
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
}