Windows runs managed hooks through a shell, and #6078 wrapped the
launcher in `powershell -EncodedCommand` to survive spaces in profile
paths. Combined with the inner PowerShell Invoke-WebRequest post, every
hook spawned two PowerShell processes (~300ms startup each), so a hook
took ~650ms+ and fired up to 6x per turn. Codex 0.140 renders that as
lingering "Running <event> hook" rows. The earlier RC worked around it
by deleting SessionStart/UserPromptSubmit/Stop, which loses lifecycle
status fidelity.
Keep all six Codex events and make them fast instead:
- Codex runs hooks as `cmd.exe /C <command>` and forwards our string
verbatim when it has no spaces/quotes, so emit the bare .cmd path for
cmd-safe profiles (zero shell startup) and fall back to the encoded
PowerShell launcher only for spaced/metachar paths (#6078 robustness).
- Replace the inner PowerShell post with curl.exe (Windows 10 1803+),
posting the same form fields as the POSIX hook and reading the raw
payload from stdin via `--data-urlencode payload@-` so UTF-8 (e.g. CJK)
survives without code-page translation.
Result: the common Codex case is 0 PowerShell (~70-150ms vs ~650ms);
spaced-path profiles drop to 1.
Claude runs hooks through Git Bash, so its launcher must stay
PowerShell-encoded (a bare path is what breaks it), but its inner post
moves to curl.exe too, cutting Claude from two PowerShell startups to
one.
Validated against the real Codex 0.140 binary (interactive TUI + exec):
all managed hooks fire, render briefly, and clear with no lingering
rows; the listener receives every post in 1-6ms.
Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(codex): wrap Windows hook command in cmd.exe to survive spaces in profile path (#6078)
Windows splits raw hook commands on whitespace, so a user profile path
like `C:\Users\Jane Doe` made Codex hooks exit with code 1. Add a
wrapWindowsHookCommand helper that invokes the .cmd through
`cmd.exe /d /c call "..."` and use it in getManagedCommand.
* fix(agent-hooks): wrap Windows hook command in cmd.exe for all agents with raw .cmd path (#6078)
Apply the wrapWindowsHookCommand helper to cursor, command-code, gemini,
grok, and droid, which shared the same raw-scriptPath-on-Windows pattern
as codex. A user profile path with a space (e.g. `C:\Users\Jane Doe`)
used to split at the space and fail with exit code 1.
Agents that already handle spaces correctly are left untouched:
- claude/openclaude (Git Bash + forward slashes)
- copilot (PowerShell with quoted path)
- kimi (Git Bash + forward slashes)
- antigravity (event-specific wrapper .cmd files)
- devin (already wraps via `cmd /d /s /c ""...""`)
Each fixed agent gets a Windows-only test asserting the cmd.exe wrapping
survives spaces in the profile path.
* fix(claude): wrap Windows hook command in cmd.exe to survive spaces in profile path (#6078)
Claude Code runs hooks through Git Bash on Windows. The previous
forward-slash trick only works when the path has no spaces — Git Bash
splits `C:/Users/Jane Doe/...` at the space and tries to execute
`C:/Users/Jane` as a command. Use wrapWindowsHookCommand so the .cmd is
invoked through `cmd.exe /d /c call "..."`, which Git Bash treats as one
argument. Applies to both Claude and OpenClaude (shared getManagedCommand).
* Harden Windows agent hook launcher
---------
Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
Speed up Windows CLI launch by delivering short startup commands through shell args, falling back to stdin for oversized commands, and moving Codex historical session bridging into an incremental background task. Includes review fixes for Windows symlink and WSL test reliability.
Route Codex terminal launches through stable active CODEX_HOME pointers for host and WSL launch homes, with account-switch repointing and validated Windows/WSL behavior.
Fix Windows launch-home file linking by using hard links for shared Codex files, handling EEXIST/idempotency, and covering the Windows SQLite sidecar behavior.
Generated agent hook POSTs now use short curl and PowerShell request timeouts so best-effort status hooks cannot hold agent subprocesses open on a stalled local listener.
* fix(shell-ready): honor ZDOTDIR without breaking zsh scoping
Fixes#1866
This reimplements PR #1737 (reverted in #1864) with a safer approach
that preserves normal zsh startup semantics.
**Core fix**: Discover ZDOTDIR by sourcing user ~/.zshenv in a subshell
instead of inside a wrapper function. This preserves top-level zsh
scoping for common patterns like `typeset -U path` that broke in the
original implementation.
**Shell safety improvements**:
- Use `printf '%s\n'` instead of `echo` for capturing ZDOTDIR (handles
special characters in paths safely)
- Subshell isolates early returns and side effects from wrapper
**Code quality**:
- Extract duplicated zsh wrapper template to `src/main/shell-templates.ts`
- Both local-pty and daemon paths now share identical wrapper logic
**Test coverage**:
- Add live zsh subprocess tests that spawn real zsh to verify:
- XDG ZDOTDIR discovery works
- `typeset -U path` in .zshrc preserves top-level scoping
- Early returns in .zshenv don't crash the wrapper
- Vanilla (non-XDG) configs fall back to HOME correctly
- Template structure tests validate subshell discovery logic
Before (broken):
```zsh
__orca_source_user_zshenv() {
source "$HOME/.zshenv" # typeset becomes function-scoped
}
```
After (fixed):
```zsh
_orca_discovered_zdotdir=$(
unset ZDOTDIR
[[ -f "$HOME/.zshenv" ]] && source "$HOME/.zshenv" 2>/dev/null
printf '%s\n' "${ZDOTDIR}"
)
export ORCA_ORIG_ZDOTDIR="${_orca_discovered_zdotdir:-${_orca_spawn_orig_zdotdir:-$HOME}}"
```
The subshell sources .zshenv at top-level (preserving normal scoping),
captures only the ZDOTDIR value, then exits. User rcfiles (.zshrc, etc.)
are still sourced at the wrapper's top level, so all scoping works normally.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Add shell-script-literal test framework and improvements
Adds a new declarative test framework for shell-ready tests that uses
literal shell scripts (copy-pastable into terminals) with inline snapshots.
Framework features:
- Shell scripts as string literals with # Run: marker to split setup/test
- Direct script execution (no brittle parsing) via temp files
- Path normalization for reproducible snapshots (<HOME>, <WRAPPER_DIR>)
- Auto-detects shell from command, supports bash/zsh/sh
- Inline snapshot testing with vitest toMatchInlineSnapshot()
Code quality improvements:
- Extract escapeRegex to shared string-utils.ts (deduplicates 2 copies)
- Refactor shell-templates.ts for readability (condense comments, add structure)
- Pre-compile regex patterns to avoid hot-path allocation
- Fix path normalization to sort by length (prevent nested path corruption)
- Fix actualUserHome handling to skip empty values
All tests passing (64/64 shell-ready tests, 55/55 affected tests).
Files added:
- src/main/providers/__tests__/shell-ready-framework/shell-script-test.ts
- src/main/providers/__tests__/shell-ready-framework/README.md
- src/main/providers/__tests__/shell-ready-framework-example.test.ts
- src/shared/string-utils.ts
Files modified:
- src/main/shell-templates.ts (readability cleanup)
- src/main/codex/config-toml-trust.ts (use shared escapeRegex)
- src/main/daemon/shell-ready.test.ts (updated for new framework)
- src/main/providers/local-pty-shell-ready.test.ts (updated for new framework)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(shell-ready): preserve zshenv semantics
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>