From c1a7748267e701e8251cd18ec95aa38b7c5c4254 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:08:58 -0700 Subject: [PATCH] fix(agent-hooks): stop the Windows hook launcher spelling the AV-denied flag triple (#16576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(agent-hooks): stop the hook launcher spelling the AV-denied flag triple Orca's Windows agent-hook launcher ran powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden \ -EncodedCommand That exact combination is the textbook "hidden encoded PowerShell" malware shape, and endpoint security denies it at process creation whatever the payload decodes to -- even `exit 0`. Every injected hook then failed with `powershell.exe: Permission denied` (exit 126 from bash's execve, EACCES) on every turn, for both Claude Code and Codex, with no AV exclusion that re-enabled it. Dropping any one of the three flags clears the signature. `-ExecutionPolicy Bypass` is the one that can move: it sets the Process scope, and so does `Set-ExecutionPolicy -Scope Process`, which now rides inside the encoded payload. `-EncodedCommand` is never policy-gated, so the bypass always gets to run before the managed script does -- which is what keeps Copilot's .ps1 hook working under a Restricted or AllSigned machine policy. The hidden window and the encoding are unchanged, so nothing regresses for #14815, #14818 or #6078. Closes #16003 * fix(agent-hooks): ship the launcher shape #16003 actually measured as allowed The previous revision of this branch dropped only `-ExecutionPolicy Bypass` and kept `-WindowStyle Hidden -EncodedCommand`, on the reasoning that "dropping any one of the three flags clears the signature". That sentence is not in the bisect. The reporter ran exactly four command lines on the affected Kaspersky/Windows 11 host: -NoProfile -WindowStyle Hidden -Command 'exit 0' -> 0 -NoProfile -EncodedCommand -> 0 -NoProfile -ExecutionPolicy Bypass -Command 'exit 0' -> 0 -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand -> 126 Every passing row drops two flags. No row drops exactly one, so the shape the branch was about to ship had never been executed on the machine that reports the bug -- and it is `-WindowStyle Hidden -EncodedCommand`, which is the "hidden encoded PowerShell" pair the denial is named for in our own comment. Shipping it would have closed #16003 while leaving every hook on that host dying at CreateProcess, with no tracking left open. So emit the measured-passing encoded row instead: `-NoProfile -EncodedCommand`. Of the two flags there was a choice between, `-EncodedCommand` is the one that carries correctness -- it is what keeps paths and switches intact across cmd.exe and MSYS (#6078, #14815). `-WindowStyle Hidden` costs at most a console flash, and only where the parent has no console to inherit. Second, the relocated bypass now runs inside try/catch. Under a MachinePolicy or UserPolicy GPO scope, `Set-ExecutionPolicy -Scope Process` reports that the process scope did not take. `-ErrorAction SilentlyContinue` covers only the non-terminating half of that; the command-line switch it replaces was silent either way. This file already documents that non-stdout PowerShell streams corrupt consumers merging our output into JSON stdout, so a per-invocation ErrorRecord on stderr is a regression we should not trade for the switch. Refs #16003 * fix(agent-hooks): keep the hook console hidden while dropping the AV-denied flag Round 2 of this PR widened the fix from "stop spelling -ExecutionPolicy Bypass" to "stop spelling it and -WindowStyle Hidden", on the reasoning that the #16003 reporter never measured a shape that drops exactly one flag, so keeping the hidden+encoded pair would be extrapolation. That trades a reproduced regression for an unmeasured one. Window suppression is the shipped fix for #14815 and its four duplicates (#14828, #15117, #15447, #15767): a hook launched from a parent with no console gets a fresh console per event, which takes foreground and eats whatever the user is typing into Orca, and never closes at all on the stdin-blocking path hook-stdin-contract.ts exists to guard. That fires on every prompt, tool call and stop of every managed agent. The AV denial, by contrast, is measured only for the full triple; that the remaining pair still trips it is a hypothesis. Between a certain regression and a possible one, keep the certainty. So the flag that leaves the command line is the policy bypass alone — the only one of the three with an exact in-payload equivalent, hence the only one that can move without losing behaviour. If the pair turns out to be denied too, the answer is a different shape that still hides the window. * fix(agent-hooks): silence progress before the policy bypass can autoload (#16621) Hardware-measured on Windows 11 while exercising #16576. Set-ExecutionPolicy autoloads Microsoft.PowerShell.Security, and that module's "Preparing modules for first use." progress record is written before any later assignment can suppress it. Running the bypass first therefore defeated the silencer that runs immediately after it: bypass-first stderr = 616 bytes, first merged line '#< CLIXML' silencer-first stderr = 0 bytes, first merged line '{"decision":"approve"}' That is precisely the corruption HOOK_PROGRESS_SILENCER's own comment warns about -- redirected progress becoming CLIXML that can corrupt merged JSON -- so the PR reintroduced the hazard it documents, one line below documenting it. Both existing tests asserted the broken order, so they enforced the bug rather than catching it. Reordered them and added one that pins the ordering itself rather than the literal string, since the string will drift again. --- src/main/agent-hooks/installer-utils.test.ts | 17 +++- .../agent-hooks/runtime-home-hook-command.ts | 2 +- .../windows-powershell-hook-launcher.test.ts | 96 +++++++++++++++++++ .../windows-powershell-hook-launcher.ts | 60 ++++++++++-- src/main/claude/hook-service.test.ts | 4 +- .../hook-service-managed-install.test.ts | 2 +- src/main/command-code/hook-service.test.ts | 2 +- src/main/cursor/hook-service.test.ts | 2 +- src/main/droid/hook-service.test.ts | 2 +- src/main/gemini/hook-service.test.ts | 2 +- src/main/grok/hook-service.test.ts | 2 +- 11 files changed, 174 insertions(+), 17 deletions(-) create mode 100644 src/main/agent-hooks/windows-powershell-hook-launcher.test.ts diff --git a/src/main/agent-hooks/installer-utils.test.ts b/src/main/agent-hooks/installer-utils.test.ts index 64b47046ec1..8515f415fd2 100644 --- a/src/main/agent-hooks/installer-utils.test.ts +++ b/src/main/agent-hooks/installer-utils.test.ts @@ -604,7 +604,7 @@ describe('wrapPosixHookCommand', () => { }) const qualifiedWindowsPowerShellCommand = - /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand \S+$/ + /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -WindowStyle Hidden -EncodedCommand \S+$/ function decodeWindowsHookCommand(command: string): string { const encodedCommand = command.match(/ -EncodedCommand (\S+)$/)?.[1] @@ -614,8 +614,10 @@ function decodeWindowsHookCommand(command: string): string { function expectedDecodedWindowsHookCommand(scriptPath: string): string { const quoted = `'${scriptPath.replaceAll("'", "''")}'` + // Why: the execution-policy bypass rides in the payload, not on the command + // line, so the launcher cannot spell the AV-blocked flag triple (#16003). // Why: PowerShell progress CLIXML corrupts consumers that merge stderr into JSON stdout. - return `$ProgressPreference='SilentlyContinue'; if (Test-Path -LiteralPath ${quoted} -PathType Leaf) { & ${quoted}; exit $LASTEXITCODE }; [Console]::In.ReadToEnd() | Out-Null; exit 0` + return `$ProgressPreference='SilentlyContinue'; try { Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force -ErrorAction SilentlyContinue } catch {}; if (Test-Path -LiteralPath ${quoted} -PathType Leaf) { & ${quoted}; exit $LASTEXITCODE }; [Console]::In.ReadToEnd() | Out-Null; exit 0` } describe('wrapWindowsHookCommand', () => { @@ -742,6 +744,17 @@ describe('wrapRuntimeHomeHookCommand', () => { } ) + it('hides the console on the Git Bash branch too, and still avoids the denied triple', () => { + // Why: this branch launches PowerShell from bash, where the parent has no + // console to inherit — Windows allocates a fresh one per hook event unless + // the switch says otherwise (#14815), and the AV verdict on the flag triple + // applies to the exact same string (#16003). + const command = wrapRuntimeHomeHookCommand('claude-hook') + + expect(command).toContain('powershell.exe" -NoProfile -WindowStyle Hidden -EncodedCommand ') + expect(command).not.toMatch(/-ExecutionPolicy/i) + }) + it('rejects a script base name that could inject shell syntax', () => { expect(() => wrapRuntimeHomeHookCommand('claude-hook; echo injected')).toThrow( 'Invalid managed script base name' diff --git a/src/main/agent-hooks/runtime-home-hook-command.ts b/src/main/agent-hooks/runtime-home-hook-command.ts index 321c8d4adef..3a6f0d20725 100644 --- a/src/main/agent-hooks/runtime-home-hook-command.ts +++ b/src/main/agent-hooks/runtime-home-hook-command.ts @@ -24,7 +24,7 @@ export function wrapRuntimeHomeHookCommand( const powershellFallback = options.neutralJsonWhenMissing ? "; Write-Output '{}'" : '' const powershellCommand = `$homePath = $env:HOME -replace '^/([A-Za-z])/', '$1:/'; $scriptPath = Join-Path $homePath '.orca\\agent-hooks\\${scriptBaseName}.cmd'; if (Test-Path -LiteralPath $scriptPath -PathType Leaf) { & $scriptPath; exit $LASTEXITCODE }; [Console]::In.ReadToEnd() | Out-Null${powershellFallback}; exit 0` const encodedCommand = encodeWindowsPowerShellHookCommand(powershellCommand) - // Why: the Git Bash and native Windows launchers must suppress windows identically (#14815). + // Why: the Git Bash and native Windows launchers must spell the same switches — window suppression (#14815) and an AV verdict on the shape (#16003) both hit either path. const powershellInvocation = `${powershell} ${WINDOWS_POWERSHELL_HOOK_SWITCHES} -EncodedCommand ${encodedCommand}` const encodedWindowsBranch = `if [ -f ${powershell} ]; then ${powershellInvocation}; else ${missingScriptFallback}; fi` const windowsBranch = `if [ -f ${windowsScript} ]; then case "\${HOME-}" in ${WINDOWS_GIT_BASH_RUNTIME_HOME_UNSAFE}) ${encodedWindowsBranch} ;; *) ${windowsScript} ;; esac; else ${missingScriptFallback}; fi` diff --git a/src/main/agent-hooks/windows-powershell-hook-launcher.test.ts b/src/main/agent-hooks/windows-powershell-hook-launcher.test.ts new file mode 100644 index 00000000000..e58fd25c960 --- /dev/null +++ b/src/main/agent-hooks/windows-powershell-hook-launcher.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest' +import { + encodeWindowsPowerShellHookCommand, + WINDOWS_POWERSHELL_HOOK_SWITCHES, + wrapWindowsPowerShellEncodedCommand +} from './windows-powershell-hook-launcher' + +function decodePayload(command: string): string { + const encoded = command.match(/ -EncodedCommand (\S+)$/)?.[1] + expect(encoded).toBeTruthy() + return Buffer.from(encoded!, 'base64').toString('utf16le') +} + +/* + * Two reproduced Windows failures constrain this one string, in opposite + * directions: + * + * #16003 — endpoint security (Kaspersky Premium, Windows 11) denies process + * creation for `-ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand` + * whatever the payload decodes to, and no exclusion re-enabled it. The triple + * must stop being spelled. + * + * #14815 (+ #14828, #15117, #15447, #15767) — without `-WindowStyle Hidden` + * every hook event allocates a console that takes foreground and eats the + * user's keystrokes, and strands a visible window outright when the hook blocks + * on stdin. Window suppression must stay. + * + * Both hold only if the flag that leaves the command line is the policy bypass, + * which is the one with an exact in-payload equivalent. + */ +describe('windows PowerShell hook launcher', () => { + it('never spells the denied flag triple on the command line', () => { + const command = wrapWindowsPowerShellEncodedCommand('exit 0') + + expect(WINDOWS_POWERSHELL_HOOK_SWITCHES).not.toMatch(/-ExecutionPolicy/i) + expect(command.replace(/ -EncodedCommand \S+$/, '')).not.toMatch(/-ExecutionPolicy/i) + }) + + it('keeps hiding the console window on every hook event (#14815)', () => { + // Dropping this flag is not a cosmetic flash: the console takes foreground + // and swallows what the user is typing into Orca (#14828), and never closes + // at all when the hook blocks reading stdin (#14815). + const command = wrapWindowsPowerShellEncodedCommand('exit 0') + + expect(WINDOWS_POWERSHELL_HOOK_SWITCHES).toBe('-NoProfile -WindowStyle Hidden') + expect(command).toMatch(/ -NoProfile -WindowStyle Hidden -EncodedCommand [A-Za-z0-9+/=]+$/) + }) + + it('keeps the execution-policy bypass, in the payload where AV cannot read it', () => { + // Why it must survive somewhere: Copilot's managed hook is a .ps1, which a + // Restricted or AllSigned machine policy refuses to run without a bypass. + // Process scope is exactly what the switch used to set. + expect(decodePayload(wrapWindowsPowerShellEncodedCommand('exit 0'))).toContain( + 'Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force -ErrorAction SilentlyContinue' + ) + }) + + it('swallows a terminating execution-policy failure, not just a non-terminating one', () => { + // A GPO MachinePolicy/UserPolicy scope makes the cmdlet complain that the + // process scope did not take. -ErrorAction covers only the non-terminating + // half; the switch this replaced printed nothing either way, and an + // ErrorRecord on stderr corrupts consumers that merge our streams into JSON. + const decoded = decodePayload(wrapWindowsPowerShellEncodedCommand('exit 0')) + + expect(decoded).toMatch(/try \{[^}]*Set-ExecutionPolicy[^}]*\} catch \{\}/) + }) + + it('applies the bypass before the caller command and keeps progress silenced', () => { + const decoded = Buffer.from( + encodeWindowsPowerShellHookCommand('& $scriptPath'), + 'base64' + ).toString('utf16le') + + expect(decoded).toBe( + "$ProgressPreference='SilentlyContinue'; try { Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force -ErrorAction SilentlyContinue } catch {}; & $scriptPath" + ) + }) + + it('silences progress before anything that can autoload a module', () => { + // Set-ExecutionPolicy pulls in Microsoft.PowerShell.Security, and its + // "Preparing modules for first use." progress record is written before a + // later assignment can suppress it. Measured on Windows 11: bypass-first put + // 616 bytes of on stderr and made "#< CLIXML" the + // first merged line -- the exact corruption HOOK_PROGRESS_SILENCER exists to + // stop. Silencer-first measured 0 bytes. + const decoded = Buffer.from( + encodeWindowsPowerShellHookCommand('& $scriptPath'), + 'base64' + ).toString('utf16le') + + expect(decoded.indexOf("$ProgressPreference='SilentlyContinue'")).toBeGreaterThanOrEqual(0) + expect(decoded.indexOf("$ProgressPreference='SilentlyContinue'")).toBeLessThan( + decoded.indexOf('Set-ExecutionPolicy') + ) + }) +}) diff --git a/src/main/agent-hooks/windows-powershell-hook-launcher.ts b/src/main/agent-hooks/windows-powershell-hook-launcher.ts index e3974ef8ff1..aeec04380db 100644 --- a/src/main/agent-hooks/windows-powershell-hook-launcher.ts +++ b/src/main/agent-hooks/windows-powershell-hook-launcher.ts @@ -1,4 +1,4 @@ -// Why: centralizing the launcher keeps window suppression consistent across installers (#14815). +// Why: centralizing the launcher keeps every installer on one command shape; #14815 and #16003 both turned on which shape it is. // Why: an absolute forward-slash path avoids PATH hijacking and survives cmd.exe and Git Bash. export function getWindowsSystem32Path(relativePath: string): string { @@ -10,16 +10,64 @@ export function getWindowsPowerShellExecutablePath(): string { return getWindowsSystem32Path('WindowsPowerShell/v1.0/powershell.exe') } -// Why: unlike conhost, hidden PowerShell relays hook output and exit status (#14818). -export const WINDOWS_POWERSHELL_HOOK_SWITCHES = - '-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden' +/** + * Switches for the PowerShell that relays hook output and exit status + * (#14818 — conhost does neither). + * + * `-WindowStyle Hidden` stays. It is the shipped fix for #14815 and its four + * duplicates (#14828, #15117, #15447, #15767): without it Windows allocates a + * console per hook event, which steals foreground from whatever the user is + * typing into, and strands a permanently visible window whenever a hook blocks + * reading stdin (see hook-stdin-contract.ts). Those are reported, reproduced + * user-facing failures on every hook event of every managed agent. + * + * `-ExecutionPolicy Bypass` is the flag that leaves the command line, because + * it is the only one of the three with an exact in-payload equivalent (below): + * it costs nothing to move. #16003 measured `-NoProfile -ExecutionPolicy Bypass + * -WindowStyle Hidden -EncodedCommand` as denied at CreateProcess (exit 126, + * Kaspersky Premium on Windows 11) no matter what the payload decodes to, so + * the triple has to stop being spelled; dropping the policy flag breaks it. + * + * What is not established: that the remaining pair clears that AV signature — + * the reporter measured no two-flag encoded shape. If it turns out not to, the + * answer is another launcher shape that still suppresses the window, not + * trading a reproduced regression for an unmeasured hope. + */ +export const WINDOWS_POWERSHELL_HOOK_SWITCHES = '-NoProfile -WindowStyle Hidden' -// Why: redirected PowerShell progress becomes CLIXML that can corrupt merged JSON output. +// Why: redirected PowerShell progress becomes CLIXML that can corrupt merged JSON +// output. It must be the FIRST statement: Set-ExecutionPolicy autoloads +// Microsoft.PowerShell.Security, whose "Preparing modules for first use." +// progress record is emitted before any later assignment can suppress it. +// Measured on Windows 11: bypass-first put 616 bytes of +// on stderr and made "#< CLIXML" the first merged line; silencer-first, 0 bytes. const HOOK_PROGRESS_SILENCER = "$ProgressPreference='SilentlyContinue'; " +/** + * Process-scope stand-in for the `-ExecutionPolicy Bypass` switch (#16003). + * + * Equivalent by construction: the switch sets the Process scope too, and both + * lose to a Group Policy scope. `-EncodedCommand` itself is never policy-gated, + * so this always gets to run; it is what lets the managed `.ps1` hooks (Copilot) + * execute under a Restricted or AllSigned machine policy. + * + * try/catch as well as `-ErrorAction SilentlyContinue`: under a MachinePolicy or + * UserPolicy GPO the cmdlet reports that the process scope did not take, and + * `-ErrorAction` only governs the non-terminating half of that. The switch this + * replaces printed nothing at all in the same situation, and an ErrorRecord on + * stderr is a live corruption risk for the consumers that merge our streams into + * JSON stdout (see the progress silencer above). A hook must still answer its + * agent when the policy is locked down. + */ +const HOOK_EXECUTION_POLICY_BYPASS = + 'try { Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force -ErrorAction SilentlyContinue } catch {}; ' + // Why: encoding shields paths and switches from cmd.exe and MSYS rewriting (#6078, #14815). export function encodeWindowsPowerShellHookCommand(command: string): string { - return Buffer.from(`${HOOK_PROGRESS_SILENCER}${command}`, 'utf16le').toString('base64') + return Buffer.from( + `${HOOK_PROGRESS_SILENCER}${HOOK_EXECUTION_POLICY_BYPASS}${command}`, + 'utf16le' + ).toString('base64') } export function wrapWindowsPowerShellEncodedCommand(command: string): string { diff --git a/src/main/claude/hook-service.test.ts b/src/main/claude/hook-service.test.ts index 2137bcda780..315ee6dd228 100644 --- a/src/main/claude/hook-service.test.ts +++ b/src/main/claude/hook-service.test.ts @@ -41,7 +41,7 @@ describe('getWindowsManagedLifecycleHook', () => { expect(hook.args).toBeUndefined() expect(hook.command).toMatch( - /\/powershell\.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden / + /\/powershell\.exe -NoProfile -WindowStyle Hidden -EncodedCommand / ) expect(hook.command).not.toContain(scriptPath) // Why: Git Bash/MSYS mangles backslash paths and slash-prefixed switches. @@ -385,7 +385,7 @@ describe('ClaudeHookService.install', () => { const hook = settings.hooks[eventName]?.[0]?.hooks?.[0] expect(hook?.args).toBeUndefined() expect(hook?.command).toMatch( - /\/powershell\.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden / + /\/powershell\.exe -NoProfile -WindowStyle Hidden -EncodedCommand / ) expect(hook?.command).not.toContain(scriptPath) diff --git a/src/main/codex/hook-service-managed-install.test.ts b/src/main/codex/hook-service-managed-install.test.ts index 22f6bf698c5..72852e45b55 100644 --- a/src/main/codex/hook-service-managed-install.test.ts +++ b/src/main/codex/hook-service-managed-install.test.ts @@ -30,7 +30,7 @@ vi.mock('os', async (importOriginal) => { import { CodexHookService } from './hook-service' const WINDOWS_POWERSHELL_LAUNCHER = - /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand \S+$/ + /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -WindowStyle Hidden -EncodedCommand \S+$/ const homes = setupCodexHookHomes(homedirMock, getPathMock) diff --git a/src/main/command-code/hook-service.test.ts b/src/main/command-code/hook-service.test.ts index ce59e20b056..f5b0f83af62 100644 --- a/src/main/command-code/hook-service.test.ts +++ b/src/main/command-code/hook-service.test.ts @@ -21,7 +21,7 @@ vi.mock('os', async () => { import { CommandCodeHookService } from './hook-service' const WINDOWS_POWERSHELL_LAUNCHER = - /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand \S+$/ + /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -WindowStyle Hidden -EncodedCommand \S+$/ describe('CommandCodeHookService', () => { let homeDir: string diff --git a/src/main/cursor/hook-service.test.ts b/src/main/cursor/hook-service.test.ts index b723d947db3..a260b74938f 100644 --- a/src/main/cursor/hook-service.test.ts +++ b/src/main/cursor/hook-service.test.ts @@ -31,7 +31,7 @@ const CURSOR_EVENTS = [ const CURSOR_SCRIPT_FILE_NAME = process.platform === 'win32' ? 'cursor-hook.cmd' : 'cursor-hook.sh' const WINDOWS_POWERSHELL_LAUNCHER = - /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand \S+$/ + /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -WindowStyle Hidden -EncodedCommand \S+$/ describe('CursorHookService', () => { let homeDir: string diff --git a/src/main/droid/hook-service.test.ts b/src/main/droid/hook-service.test.ts index 0a65facab21..bd145fdc1de 100644 --- a/src/main/droid/hook-service.test.ts +++ b/src/main/droid/hook-service.test.ts @@ -25,7 +25,7 @@ vi.mock('os', async () => { import { DroidHookService } from './hook-service' const WINDOWS_POWERSHELL_LAUNCHER = - /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand \S+$/ + /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -WindowStyle Hidden -EncodedCommand \S+$/ describe('DroidHookService', () => { let homeDir: string diff --git a/src/main/gemini/hook-service.test.ts b/src/main/gemini/hook-service.test.ts index fc5a302a7d1..a835a6278f8 100644 --- a/src/main/gemini/hook-service.test.ts +++ b/src/main/gemini/hook-service.test.ts @@ -26,7 +26,7 @@ vi.mock('os', async (importOriginal) => { import { GeminiHookService } from './hook-service' const WINDOWS_POWERSHELL_LAUNCHER = - /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand \S+$/ + /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -WindowStyle Hidden -EncodedCommand \S+$/ describe('GeminiHookService', () => { let homeDir: string diff --git a/src/main/grok/hook-service.test.ts b/src/main/grok/hook-service.test.ts index 89bd53f22c0..4b1625614ac 100644 --- a/src/main/grok/hook-service.test.ts +++ b/src/main/grok/hook-service.test.ts @@ -33,7 +33,7 @@ import { POSIX_HOOK_STDIN_READER } from '../agent-hooks/hook-stdin-contract' const GROK_SCRIPT_FILE_NAME = process.platform === 'win32' ? 'grok-hook.cmd' : 'grok-hook.sh' const WINDOWS_POWERSHELL_LAUNCHER = - /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand \S+$/ + /^[A-Za-z]:\/[^"]*\/System32\/WindowsPowerShell\/v1\.0\/powershell\.exe -NoProfile -WindowStyle Hidden -EncodedCommand \S+$/ // Why (#14828): Windows registers the bare script path when it is cmd-safe and only falls back // to the encoded launcher for a profile path that is not (#6078). windows-hook-launcher-chain