fix(hooks): register the Claude hook script directly on Windows (#18875)

The Windows Claude Code lifecycle hook was registered as
`powershell.exe -NoProfile -EncodedCommand <...>` whose entire decoded payload
was a `Test-Path` and a call to `~/.orca/agent-hooks/claude-hook.cmd`. Every
hook event paid a full PowerShell start-up to reach a script that exits at its
first `ORCA_PANE_KEY` guard, so sessions outside Orca paid it to do nothing.

Register the script path itself instead, with `|| echo {}` for the
neutral-JSON-when-missing contract (#14818). Measured on Windows 11, invoked as
Claude Code invokes it (`printf payload | bash -c -l "<command>"`):

  idle (n=12)          baseline 177ms | before 471ms | after 213ms
  10-way conc (n=40)            --    | before 656ms | after 296ms
  p95 under load                --    | before 696ms | after 337ms

It also drops an interpreter from the chain the hook's timeout kill must tear
down. Killing the hook does not kill its PowerShell grandchild, which still
holds the stdout handle the agent reads to EOF -- measured, EOF arrived 352ms
AFTER the kill, when the orphan exited by itself. msys2 creates children
suspended and resumes them after, so a kill landing in that window strands one
that never exits and EOF never comes; that is the reported frozen session.

The encoded launcher stays as the fallback for profile paths the shells cannot
carry bare (space, `%`, `^`, `&`, non-ASCII) and for hosts where Git Bash is not
resolvable, because PowerShell 5.1 rejects `||`. Every other agent's hook is
untouched, as is the remote/SSH path.

Not adopted from the report: `cmd.exe /d /c <path>` (MSYS rewrites the `/c`
under Git Bash -- measured, the invocation fails), and raising the 10s timeout
(the orphan survives the kill regardless; the fast path puts the hook 30x under
the budget so the kill effectively stops firing).
This commit is contained in:
Orca Worker
2026-09-05 17:41:13 -07:00
parent 5238a4d576
commit e3c5bc02a9
7 changed files with 377 additions and 20 deletions
+23 -1
View File
@@ -166,7 +166,8 @@ What remains is `-EncodedCommand` without the bypass: the PTY bootstraps
`src/main/providers/windows-shell-args.ts`), the hook wrappers
(`src/main/agent-hooks/windows-powershell-hook-launcher.ts` and its callers
`src/main/agent-hooks/runtime-home-hook-command.ts`,
`src/main/agent-hooks/installer-utils.ts`, `src/main/claude/hook-settings.ts`),
`src/main/agent-hooks/installer-utils.ts`, and `src/main/claude/hook-settings.ts`
— that last one only as a *fallback* since #18875, see below),
`src/main/runtime/windows-default-route-interfaces.ts`,
`src/main/runtime/orchestration/setup-completion-signal.ts`,
`src/shared/hermes-startup-query.ts`, and the four ex-bypass sites above.
@@ -242,6 +243,27 @@ breadth: every interpreter hop between Orca and the thing the user asked for add
a scored edge, which is why the shipped doctrine of #15520 and #15595 is to
*shorten the interpreter chain* rather than to hide a window.
#18875 is a worked example of that doctrine. The Claude Code lifecycle hook was
registered as `powershell.exe -NoProfile -EncodedCommand <...>` whose entire
decoded payload was a `Test-Path` and a call to `~/.orca/agent-hooks/claude-hook.cmd`.
It now registers the script path itself (`<path> || echo {}`), so `bash ->
powershell -> cmd -> curl` became `bash -> cmd -> curl` and one
`powershell.exe -EncodedCommand` per hook event — a first-class Defender alert
title — leaves the tree. The reporting box fired ~6 900 of them in five days,
70% from Claude sessions that were not running under Orca at all and whose hook
exits at its first `ORCA_PANE_KEY` guard.
What is measured is latency and the hop count, nothing else: median 471 ms ->
213 ms per event idle, and 656 ms -> 296 ms (p95 696 ms -> 337 ms) under 10-way
concurrency, invoked as Claude Code invokes it. **No EDR verdict on either tree
was measured**, so claim the removed `-EncodedCommand` spelling and the shorter
chain, not a score. `cmd.exe` remains in the tree, spelled by MSYS's own `.cmd`
spawn rather than by us — the doc's one "unavoidable for `.cmd`/`.bat`" case,
carrying an absolute path and two literal tokens, with no caret escaping, no
encoding and no free text. The encoded launcher is still the shape for profile
paths the shells cannot carry bare (a space, `%`, `^`, `&`, non-ASCII) and for
hosts where Git Bash is not resolvable, because PowerShell 5.1 rejects `||`.
### Computer use: screen capture, synthetic input, runtime-compiled MSIL
`native/computer-use-windows/runtime.ps1` is a large PowerShell script.