mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
fix(agent-hooks): wrap Windows hook commands in cmd.exe to survive spaces in profile path (#6078) (#6083)
* 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>
This commit is contained in:
co-authored by
Jinwoo Hong
parent
d4ae7b2392
commit
1fde7f553c
@@ -48,15 +48,46 @@ describe('CommandCodeHookService', () => {
|
||||
expect(config.hooks.PreToolUse[0].matcher).toBe('.*')
|
||||
expect(config.hooks.PostToolUse[0].matcher).toBe('.*')
|
||||
expect(config.hooks.Stop[0].matcher).toBeUndefined()
|
||||
expect(config.hooks.PreToolUse[0].hooks[0].command).toContain('command-code-hook')
|
||||
expect(config.hooks.PreToolUse[0].hooks[0].command).toContain(join(homeDir, '.orca'))
|
||||
if (process.platform === 'win32') {
|
||||
expect(config.hooks.PreToolUse[0].hooks[0].command).toContain('command-code-hook.cmd')
|
||||
} else {
|
||||
expect(config.hooks.PreToolUse[0].hooks[0].command).toMatch(
|
||||
process.platform === 'win32'
|
||||
? /^powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand \S+$/
|
||||
: /command-code-hook/
|
||||
)
|
||||
if (process.platform !== 'win32') {
|
||||
expect(config.hooks.PreToolUse[0].hooks[0].command).toContain(join(homeDir, '.orca'))
|
||||
}
|
||||
if (process.platform !== 'win32') {
|
||||
expect(config.hooks.PreToolUse[0].hooks[0].command).toMatch(/^if \[ -x /)
|
||||
}
|
||||
})
|
||||
|
||||
// Why: #6078 — a Windows user profile path with a space used to be written
|
||||
// verbatim as the hook command, so the agent split it at the space. The
|
||||
// managed command must use an encoded launcher so the path never appears raw
|
||||
// on the cmd.exe command line.
|
||||
it.skipIf(process.platform !== 'win32')(
|
||||
'wraps the managed hook command to survive spaces in the profile path (#6078)',
|
||||
() => {
|
||||
const spaceHome = join(tmpdir(), 'orca command-code home with spaces')
|
||||
mkdirSync(spaceHome, { recursive: true })
|
||||
homedirMock.mockReturnValue(spaceHome)
|
||||
try {
|
||||
expect(new CommandCodeHookService().install().state).toBe('installed')
|
||||
|
||||
const config = JSON.parse(
|
||||
readFileSync(join(spaceHome, '.commandcode', 'settings.json'), 'utf8')
|
||||
) as { hooks: Record<string, { hooks: { command: string }[] }[]> }
|
||||
|
||||
const command = config.hooks.PreToolUse[0].hooks[0].command
|
||||
expect(command).toMatch(
|
||||
/^powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand \S+$/
|
||||
)
|
||||
} finally {
|
||||
rmSync(spaceHome, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('installs a hook script that can recover the endpoint when Command Code strips token env', () => {
|
||||
new CommandCodeHookService().install()
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
readHooksJson,
|
||||
removeManagedCommands,
|
||||
wrapPosixHookCommand,
|
||||
wrapWindowsHookCommand,
|
||||
writeHooksJson,
|
||||
writeManagedScript,
|
||||
type HookDefinition
|
||||
@@ -45,7 +46,9 @@ function getManagedScriptPath(): string {
|
||||
}
|
||||
|
||||
function getManagedCommand(scriptPath: string): string {
|
||||
return process.platform === 'win32' ? scriptPath : wrapPosixHookCommand(scriptPath)
|
||||
return process.platform === 'win32'
|
||||
? wrapWindowsHookCommand(scriptPath)
|
||||
: wrapPosixHookCommand(scriptPath)
|
||||
}
|
||||
|
||||
function getManagedScript(target: 'local' | 'posix' = 'local'): string {
|
||||
|
||||
Reference in New Issue
Block a user