diff --git a/app/src/main/hl/engines/pathEnrich.ts b/app/src/main/hl/engines/pathEnrich.ts
index 6f47b2e8..e918afe9 100644
--- a/app/src/main/hl/engines/pathEnrich.ts
+++ b/app/src/main/hl/engines/pathEnrich.ts
@@ -246,14 +246,15 @@ function findOnWindowsPath(name: string, env: NodeJS.ProcessEnv): string | null
// PATHEXT is the canonical search order. We always check `.exe` first so
// a native binary wins over an npm shim with the same stem.
const pathExt = (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').map((e) => e.toLowerCase()).filter(Boolean);
- // Always include .ps1 since npm-installed global CLIs may ship as PowerShell
- // scripts on Windows (e.g. Claude Code's claude.ps1). Insert it before .bat/.cmd
- // so native binaries still win, but working .ps1 shims take precedence over
- // potentially broken/dummy .cmd stubs generated by older npm versions.
+ // npm-installed global CLIs typically ship .cmd, .ps1, AND a sh wrapper
+ // (e.g. claude.cmd + claude.ps1). When both .cmd and .ps1 are present we
+ // route through cmd.exe — it's already on COMSPEC, doesn't depend on
+ // env.Path-based powershell.exe lookup (which has been observed to fail
+ // with ENOENT inside Electron's main process), and avoids PowerShell's
+ // cold-start cost (~1-2s + Defender script scan). .ps1 stays as a
+ // fallback for packages that ship only PowerShell shims.
if (!pathExt.includes('.ps1')) {
- const batIdx = pathExt.findIndex((e) => e === '.bat' || e === '.cmd');
- if (batIdx >= 0) pathExt.splice(batIdx, 0, '.ps1');
- else pathExt.push('.ps1');
+ pathExt.push('.ps1');
}
const exts = name.includes('.') && pathExt.includes(path.win32.extname(name).toLowerCase())
? ['']
@@ -313,8 +314,16 @@ export function resolveCliSpawn(
if (ext === '.ps1') {
// PowerShell scripts: route through powershell.exe so execution policy
// and profile loading don't block npm-installed CLIs like claude.ps1.
+ // Use the absolute path under %SystemRoot% rather than relying on
+ // env.Path / Path: Electron's main process has been observed to spawn
+ // with `spawn powershell.exe ENOENT` even when the WindowsPowerShell
+ // dir is on the system PATH, because libuv on Windows reads the env
+ // block's `Path` key (capitalized) and our enriched env may only
+ // expose `PATH`. The absolute path sidesteps that lookup entirely.
+ const systemRoot = env.SystemRoot ?? env.SYSTEMROOT ?? 'C:\\Windows';
+ const absPwsh = path.win32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
return {
- command: 'powershell.exe',
+ command: absPwsh,
args: ['-ExecutionPolicy', 'Bypass', '-NoProfile', '-File', resolved, ...args],
viaCmdShell: false,
spawnOptions: {},
diff --git a/app/src/main/hl/stock/browser-harness-js/sdk/browser-harness-js.cmd b/app/src/main/hl/stock/browser-harness-js/sdk/browser-harness-js.cmd
new file mode 100644
index 00000000..e91e699d
--- /dev/null
+++ b/app/src/main/hl/stock/browser-harness-js/sdk/browser-harness-js.cmd
@@ -0,0 +1,44 @@
+@echo off
+rem browser-harness-js Windows launcher — runs the bundled bash script under
+rem Git for Windows' bash.exe. The script is POSIX-only (curl, nohup, /tmp,
+rem bun bootstrap) so we delegate to bash rather than re-implement in cmd.exe.
+rem
+rem Discovery order:
+rem 1. %BROWSER_HARNESS_JS_BASH% (explicit env override)
+rem 2. %ProgramFiles%\Git\bin\bash.exe
+rem 3. %ProgramFiles(x86)%\Git\bin\bash.exe
+rem 4. %LocalAppData%\Programs\Git\bin\bash.exe (per-user install)
+rem
+rem Without Git for Windows, exits 1 with a stderr hint so the agent sees a
+rem clean error instead of triggering Windows' "Open with..." association
+rem dialog on the extensionless bash script next to this file.
+
+setlocal
+
+set "SCRIPT_DIR=%~dp0"
+set "BASH_SCRIPT=%SCRIPT_DIR%browser-harness-js"
+
+rem `exit /b` (no arg) propagates the *current* ERRORLEVEL. Inside a
+rem parenthesized block, `exit /b %errorlevel%` would expand at parse time
+rem and silently report 0 even if bash failed.
+
+if defined BROWSER_HARNESS_JS_BASH (
+ if exist "%BROWSER_HARNESS_JS_BASH%" (
+ "%BROWSER_HARNESS_JS_BASH%" "%BASH_SCRIPT%" %*
+ exit /b
+ )
+)
+
+for %%P in (
+ "%ProgramFiles%\Git\bin\bash.exe"
+ "%ProgramFiles(x86)%\Git\bin\bash.exe"
+ "%LocalAppData%\Programs\Git\bin\bash.exe"
+) do (
+ if exist %%P (
+ %%P "%BASH_SCRIPT%" %*
+ exit /b
+ )
+)
+
+>&2 echo browser-harness-js: bash.exe not found. Install Git for Windows from https://gitforwindows.org/ or set BROWSER_HARNESS_JS_BASH to a bash.exe path.
+exit /b 1
diff --git a/app/src/renderer/hub/ConnectionsPane.tsx b/app/src/renderer/hub/ConnectionsPane.tsx
index 417cd7ec..881d0497 100644
--- a/app/src/renderer/hub/ConnectionsPane.tsx
+++ b/app/src/renderer/hub/ConnectionsPane.tsx
@@ -1181,6 +1181,15 @@ export function ConnectionsPane({
+ {/*
+ Cookie sync is unsupported on Windows: Chromium 127+ uses App-Bound
+ Encryption (v20) tied to the original user-data-dir, so a temp-copy
+ profile decrypts to nothing, and launching headless against the real
+ profile is blocked by the Chromium DevTools hardening that refuses
+ --remote-debugging-port for the default user-data-dir. Hide the
+ section entirely on win32 until we have a native v20 decryption path.
+ */}
+ {window.electronAPI?.shell?.platform !== 'win32' && (
)}
+ )}
);
}
diff --git a/app/src/renderer/hub/SettingsPane.tsx b/app/src/renderer/hub/SettingsPane.tsx
index 629f5307..8d1fe693 100644
--- a/app/src/renderer/hub/SettingsPane.tsx
+++ b/app/src/renderer/hub/SettingsPane.tsx
@@ -418,6 +418,11 @@ export function SettingsPane({ intent, keybindings, overrides, onUpdateBinding,
const scrollerRef = useRef(null);
const [activeSection, setActiveSection] = useState('settings-application');
const platform = window.electronAPI?.shell?.platform ?? fallbackShortcutPlatform();
+ // Cookie sync is unsupported on Windows (Chromium ABE + DevTools hardening),
+ // so the Browser Sync tab + section are hidden on win32.
+ const tabs = platform === 'win32'
+ ? SETTINGS_TABS.filter((tab) => tab.id !== 'settings-browser-sync')
+ : SETTINGS_TABS;
const scrollToSection = useCallback((id: SettingsSectionId, behavior: ScrollBehavior = 'smooth') => {
const scroller = scrollerRef.current;
@@ -434,14 +439,14 @@ export function SettingsPane({ intent, keybindings, overrides, onUpdateBinding,
const updateActiveFromScroll = useCallback(() => {
const scroller = scrollerRef.current;
if (!scroller) return;
- let next = SETTINGS_TABS[0].id;
+ let next = tabs[0].id;
const threshold = scroller.scrollTop + 112;
- for (const tab of SETTINGS_TABS) {
+ for (const tab of tabs) {
const section = scroller.querySelector(`#${tab.id}`);
if (section && section.offsetTop <= threshold) next = tab.id;
}
setActiveSection(next);
- }, []);
+ }, [tabs]);
useEffect(() => {
const sectionId = intent?.sectionId ?? (
@@ -467,7 +472,7 @@ export function SettingsPane({ intent, keybindings, overrides, onUpdateBinding,