From 5ce9d58b2d2a3eb3401e1a92d077264b2f4a7fbb Mon Sep 17 00:00:00 2001 From: Mohd Irshad <117459364+siddiqueirshad@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:47:08 +0530 Subject: [PATCH] fix(agent-hooks): force C numeric locale on POSIX curl timeouts (#19600) * fix(agent-hooks): force C numeric locale on POSIX curl timeouts curl parses --connect-timeout/--max-time with locale-sensitive strtod, so comma-decimal locales reject 0.5 and every hook silently falls into the restart spool. Fixes #19578 Co-authored-by: Cursor * fix(agent-hooks): neutralize LC_ALL before setting LC_NUMERIC=C on POSIX curl Signed-off-by: siddiqui irshad * style(agent-hooks): format scoped curl commands * test(hooks): expect scoped curl locale in Muse install script --------- Signed-off-by: siddiqui irshad Co-authored-by: Cursor Co-authored-by: Neil --- src/main/agent-hooks/hook-post-command.ts | 7 +- src/main/agent-hooks/installer-utils.test.ts | 7 ++ .../managed-hook-script-refresh.test.ts | 27 ++++++ .../agent-hooks/managed-hook-timeout.test.ts | 94 ++++++++++++++++++- src/main/antigravity/hook-script.ts | 3 +- src/main/antigravity/hook-service.test.ts | 2 +- src/main/claude/hook-service.test.ts | 2 +- src/main/claude/statusline-script.test.ts | 2 + src/main/claude/statusline-script.ts | 3 +- .../command-code-managed-script.ts | 3 +- src/main/copilot/copilot-managed-script.ts | 3 +- src/main/cursor/hook-script.ts | 3 +- src/main/cursor/hook-service.test.ts | 2 +- src/main/devin/hook-service.test.ts | 4 +- src/main/devin/hook-service.ts | 3 +- src/main/droid/hook-service.ts | 3 +- src/main/gemini/hook-service.ts | 3 +- src/main/grok/grok-hook-script.ts | 3 +- src/main/grok/hook-service.test.ts | 2 +- src/main/kimi/hook-service.test.ts | 2 +- src/main/kimi/hook-service.ts | 3 +- src/main/muse/hook-service.test.ts | 4 +- 22 files changed, 166 insertions(+), 19 deletions(-) diff --git a/src/main/agent-hooks/hook-post-command.ts b/src/main/agent-hooks/hook-post-command.ts index 8cb8e2071cb..f9e1a4f4222 100644 --- a/src/main/agent-hooks/hook-post-command.ts +++ b/src/main/agent-hooks/hook-post-command.ts @@ -1,11 +1,16 @@ import type { AgentHookSource } from '../../shared/agent-hook-relay' import { ORCA_HOOK_RAW_JSON_TRANSPORT } from '../../shared/agent-hook-types' +// Older curl needs C numeric parsing; keep the parent script's UTF-8 locale unchanged. +export function posixCurlCommand(curlCommand = 'curl'): string { + return `LC_ALL= LC_NUMERIC=C ${curlCommand}` +} + export function buildPosixAgentHookPostCommand( source: AgentHookSource, options: { curlCommand?: string; indent?: string } = {} ): string[] { - const curlCommand = options.curlCommand ?? 'curl' + const curlCommand = posixCurlCommand(options.curlCommand ?? 'curl') const indent = options.indent ?? ' ' return [ `if [ "\${ORCA_AGENT_HOOK_TRANSPORT:-}" = "${ORCA_HOOK_RAW_JSON_TRANSPORT}" ] && command -v base64 >/dev/null 2>&1 && command -v tr >/dev/null 2>&1; then`, diff --git a/src/main/agent-hooks/installer-utils.test.ts b/src/main/agent-hooks/installer-utils.test.ts index 8ace0c724e3..7b9797ad51c 100644 --- a/src/main/agent-hooks/installer-utils.test.ts +++ b/src/main/agent-hooks/installer-utils.test.ts @@ -901,6 +901,13 @@ describe('buildPosixAgentHookPostCommand', () => { } } ) + + it('forces C numeric locale on every curl invocation so fractional timeouts parse', () => { + const command = buildPosixAgentHookPostCommand('claude').join('\n') + expect(command).not.toMatch(/\|\s+curl\b/) + const custom = buildPosixAgentHookPostCommand('codex', { curlCommand: '"$curl_bin"' }) + expect(custom.join('\n')).toContain('| LC_ALL= LC_NUMERIC=C "$curl_bin" -sS') + }) }) describe('buildWindowsAgentHookCurlPostCommand', () => { diff --git a/src/main/agent-hooks/managed-hook-script-refresh.test.ts b/src/main/agent-hooks/managed-hook-script-refresh.test.ts index 0dbd69cc10a..3bc797a7d74 100644 --- a/src/main/agent-hooks/managed-hook-script-refresh.test.ts +++ b/src/main/agent-hooks/managed-hook-script-refresh.test.ts @@ -145,6 +145,33 @@ describe('managed hook script refresh', () => { } }) + it.skipIf(process.platform === 'win32')( + 'refreshes existing POSIX hook and statusline locale handling without a CLI config', + async () => { + const home = mkdtempSync(join(tmpdir(), 'orca-hook-locale-refresh-')) + homedirMock.mockReturnValue(home) + try { + const hooksDir = join(home, '.orca', 'agent-hooks') + mkdirSync(hooksDir, { recursive: true }) + for (const name of ['claude-hook.sh', 'claude-statusline.sh']) { + writeFileSync(join(hooksDir, name), '#!/bin/sh\ncurl --connect-timeout 0.5\n') + } + await new ClaudeHookService().refreshManagedScripts() + for (const name of ['claude-hook.sh', 'claude-statusline.sh']) { + const script = readFileSync(join(hooksDir, name), 'utf8') + expect(script).toContain('| LC_ALL= LC_NUMERIC=C curl -sS') + expect(script).not.toMatch(/\|\s+curl\b/) + expect(statSync(join(hooksDir, name)).mode & 0o777).toBe(0o755) + } + expect(existsSync(join(home, '.claude'))).toBe(false) + expect(readdirSync(hooksDir).sort()).toEqual(['claude-hook.sh', 'claude-statusline.sh']) + } finally { + homedirMock.mockImplementation(() => process.env.HOME ?? tmpdir()) + rmSync(home, { recursive: true, force: true }) + } + } + ) + it('covers every shared launcher script with a refresher', async () => { const home = mkdtempSync(join(tmpdir(), 'orca-hook-refresh-coverage-')) homedirMock.mockReturnValue(home) diff --git a/src/main/agent-hooks/managed-hook-timeout.test.ts b/src/main/agent-hooks/managed-hook-timeout.test.ts index a50ef9652dc..6f88d91b57f 100644 --- a/src/main/agent-hooks/managed-hook-timeout.test.ts +++ b/src/main/agent-hooks/managed-hook-timeout.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { spawn, spawnSync } from 'node:child_process' import { createServer, type Server, type Socket } from 'node:net' -import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import type { SFTPWrapper } from 'ssh2' @@ -205,12 +205,33 @@ describe('managed agent hook timeouts', () => { (process.platform === 'win32' && command.includes('-EncodedCommand')) ) expect(carriers).toBeGreaterThan(0) + if (process.platform !== 'win32') { + assertPosixCurlUsesCNumericLocale( + readFileSync(join(homeDir, '.orca', 'agent-hooks', 'droid-hook.sh'), 'utf8'), + 'droid' + ) + } } finally { homedirMock.mockImplementation(() => process.env.HOME ?? tmpdir()) rmSync(homeDir, { recursive: true, force: true }) } }) + function assertPosixCurlUsesCNumericLocale(content: string, label: string): void { + expect(content, `${label} wrapper missing curl transport`).toContain('curl') + expect(content, `${label} wrapper missing --connect-timeout`).toContain('--connect-timeout') + expect(content, `${label} wrapper missing --max-time`).toContain('--max-time') + expect(content, `${label} wrapper missing C numeric locale for curl`).toMatch( + /LC_ALL= LC_NUMERIC=C (?:curl|"\$curl_bin")/ + ) + expect(content, `${label} wrapper posts curl without C numeric locale`).not.toMatch( + /\|\s+curl\b/ + ) + expect(content, `${label} wrapper posts curl_bin without C numeric locale`).not.toMatch( + /\|\s+"\$curl_bin"/ + ) + } + it.skipIf(process.platform === 'win32')( 'passes timeout budgets to curl from every generated POSIX wrapper', async () => { @@ -232,6 +253,7 @@ describe('managed agent hook timeouts', () => { if (!path.endsWith('.sh')) { continue } + assertPosixCurlUsesCNumericLocale(content, `${agent} ${path}`) rmSync(capturePath, { force: true }) const result = spawnSync( '/bin/sh', @@ -279,6 +301,76 @@ describe('managed agent hook timeouts', () => { } ) + describe('comma-decimal curl locale', () => { + let tempDir: string | null = null + + afterEach(() => { + if (tempDir) { + rmSync(tempDir, { recursive: true, force: true }) + tempDir = null + } + }) + + it.skipIf(process.platform === 'win32')( + 'clears inherited LC_ALL and sets LC_NUMERIC only for curl', + async () => { + const { sftp, fs } = createFakeSftp() + await new GeminiHookService().installRemote(sftp, REMOTE_HOME) + const wrapperBody = fs.files.get(`${REMOTE_HOME}/.orca/agent-hooks/gemini-hook.sh`)! + + tempDir = mkdtempSync(join(tmpdir(), 'orca-hook-lc-numeric-')) + const binDir = join(tempDir, 'bin') + mkdirSync(binDir) + const observed = join(tempDir, 'lc-numeric') + writeFileSync( + join(binDir, 'curl'), + `#!/bin/sh\nprintf '%s|%s' "$LC_ALL" "$LC_NUMERIC" > "${observed}"\nexit 0\n` + ) + chmodSync(join(binDir, 'curl'), 0o755) + const scriptPath = join(tempDir, 'gemini-hook.sh') + writeFileSync(scriptPath, wrapperBody, 'utf8') + chmodSync(scriptPath, 0o755) + + await new Promise((resolve, reject) => { + const child = spawn('sh', [scriptPath], { + env: { + ...process.env, + PATH: `${binDir}:${process.env.PATH ?? ''}`, + LC_ALL: 'de_DE.UTF-8', + LC_NUMERIC: 'de_DE.UTF-8', + ORCA_AGENT_HOOK_ENDPOINT: '', + ORCA_AGENT_HOOK_PORT: '9', + ORCA_AGENT_HOOK_TOKEN: 'test-token', + ORCA_PANE_KEY: 'pane-1', + ORCA_TAB_ID: 'tab-1', + ORCA_WORKTREE_ID: 'wt-1' + }, + stdio: ['pipe', 'ignore', 'ignore'] + }) + const timeout = setTimeout(() => { + child.kill('SIGKILL') + reject(new Error('hook script exceeded test timeout')) + }, 10_000) + child.on('error', (error) => { + clearTimeout(timeout) + reject(error) + }) + child.on('close', (status) => { + clearTimeout(timeout) + if (status === 0) { + resolve() + return + } + reject(new Error(`hook script exited ${String(status)}`)) + }) + child.stdin.end('{"hook_event_name":"Stop"}') + }) + + expect(readFileSync(observed, 'utf8')).toBe('|C') + } + ) + }) + describe('dead-endpoint transport budget', () => { let tempDir: string | null = null let server: Server | null = null diff --git a/src/main/antigravity/hook-script.ts b/src/main/antigravity/hook-script.ts index 369528c0487..5a65a955097 100644 --- a/src/main/antigravity/hook-script.ts +++ b/src/main/antigravity/hook-script.ts @@ -1,3 +1,4 @@ +import { posixCurlCommand } from '../agent-hooks/hook-post-command' import { buildPosixHookPayloadCapture, POSIX_HOOK_JSON_STDIN, @@ -56,7 +57,7 @@ export function getManagedScript( ' exit 0', 'fi', // Keep large payloads off command lines to avoid EDR false positives. - 'printf \'%s\' "$payload" | curl -sS -X POST "http://127.0.0.1:${ORCA_AGENT_HOOK_PORT}/hook/antigravity" \\', + `printf '%s' "$payload" | ${posixCurlCommand()} -sS -X POST "http://127.0.0.1:\${ORCA_AGENT_HOOK_PORT}/hook/antigravity" \\`, ' --connect-timeout 0.5 --max-time 1.5 \\', ' -H "Content-Type: application/x-www-form-urlencoded" \\', ' -H "X-Orca-Agent-Hook-Token: ${ORCA_AGENT_HOOK_TOKEN}" \\', diff --git a/src/main/antigravity/hook-service.test.ts b/src/main/antigravity/hook-service.test.ts index 3d72a63faf1..06552f5a60a 100644 --- a/src/main/antigravity/hook-service.test.ts +++ b/src/main/antigravity/hook-service.test.ts @@ -113,7 +113,7 @@ describe('AntigravityHookService', () => { expect(script).toContain(`payload=$(${POSIX_HOOK_JSON_STDIN_READER})`) expect(script).toContain("payload='{}'") expect(script).not.toContain('if [ -z "$payload" ]; then\n exit 0\nfi') - expect(script).toContain('printf \'%s\' "$payload" | curl') + expect(script).toContain('printf \'%s\' "$payload" | LC_ALL= LC_NUMERIC=C curl') expect(script).toContain('--data-urlencode "payload@-"') expect(script).not.toContain('--data-urlencode "payload=${payload}"') } diff --git a/src/main/claude/hook-service.test.ts b/src/main/claude/hook-service.test.ts index 4fe5fbc88d0..b0f5f9f1d9a 100644 --- a/src/main/claude/hook-service.test.ts +++ b/src/main/claude/hook-service.test.ts @@ -724,7 +724,7 @@ describe('ClaudeHookService.installRemote', () => { expect(script!.indexOf('printf "{}\\n"')).toBe( script!.indexOf('#!/bin/sh') + '#!/bin/sh\n'.length ) - expect(script).toContain('printf \'%s\' "$payload" | curl') + expect(script).toContain('printf \'%s\' "$payload" | LC_ALL= LC_NUMERIC=C curl') expect(script).toContain('-H "Content-Type: application/json"') expect(script).toContain('orca_hook_metadata=$(printf') expect(script).toContain('unset ORCA_AGENT_HOOK_TRANSPORT') diff --git a/src/main/claude/statusline-script.test.ts b/src/main/claude/statusline-script.test.ts index 1c0d8bc9824..ba2c871fe3e 100644 --- a/src/main/claude/statusline-script.test.ts +++ b/src/main/claude/statusline-script.test.ts @@ -29,6 +29,8 @@ describe('getManagedStatusLineScript (posix)', () => { expect(endpointIndex).toBeLessThan(curlIndex) expect(script).toContain('/statusline/claude') expect(script).toContain('--data-urlencode "payload@-"') + expect(script).toContain('| LC_ALL= LC_NUMERIC=C curl -sS') + expect(script).not.toMatch(/\|\s+curl\b/) }) it('returns the posix script even on win32 when targeting a remote', () => { diff --git a/src/main/claude/statusline-script.ts b/src/main/claude/statusline-script.ts index f932fb5ee89..fce3d94ec19 100644 --- a/src/main/claude/statusline-script.ts +++ b/src/main/claude/statusline-script.ts @@ -1,3 +1,4 @@ +import { posixCurlCommand } from '../agent-hooks/hook-post-command' import { buildWindowsHookStdinDrainEpilogue, WINDOWS_HOOK_STDIN_DRAIN_LABEL, @@ -161,7 +162,7 @@ export function getManagedStatusLineScript(target: 'local' | 'posix' = 'local'): 'if [ -n "$orca_statusline_now" ]; then', ' printf \'%s\' "$orca_statusline_now" >"$orca_statusline_stamp" 2>/dev/null || :', 'fi', - `printf '%s' "$payload" | curl -sS -X POST "http://127.0.0.1:\${ORCA_AGENT_HOOK_PORT}${CLAUDE_STATUSLINE_PATHNAME}" \\`, + `printf '%s' "$payload" | ${posixCurlCommand()} -sS -X POST "http://127.0.0.1:\${ORCA_AGENT_HOOK_PORT}${CLAUDE_STATUSLINE_PATHNAME}" \\`, ' --connect-timeout 0.5 --max-time 1.5 \\', ' -H "Content-Type: application/x-www-form-urlencoded" \\', ' -H "X-Orca-Agent-Hook-Token: ${ORCA_AGENT_HOOK_TOKEN}" \\', diff --git a/src/main/command-code/command-code-managed-script.ts b/src/main/command-code/command-code-managed-script.ts index e22bf4fe352..a370482ad1b 100644 --- a/src/main/command-code/command-code-managed-script.ts +++ b/src/main/command-code/command-code-managed-script.ts @@ -1,3 +1,4 @@ +import { posixCurlCommand } from '../agent-hooks/hook-post-command' import { buildWindowsAgentHookPostCommand } from '../agent-hooks/installer-utils' import { buildPosixHookPayloadCapture, @@ -122,7 +123,7 @@ export function buildCommandCodeManagedScript( ' exit 0', 'fi', // Bound stalled posts and pipe large payloads through stdin to avoid EDR command-line false positives. - 'printf \'%s\' "$payload" | curl -sS -X POST "http://127.0.0.1:${ORCA_AGENT_HOOK_PORT}/hook/command-code" \\', + `printf '%s' "$payload" | ${posixCurlCommand()} -sS -X POST "http://127.0.0.1:\${ORCA_AGENT_HOOK_PORT}/hook/command-code" \\`, ' --connect-timeout 0.5 --max-time 1.5 \\', ' -H "Content-Type: application/x-www-form-urlencoded" \\', ' -H "X-Orca-Agent-Hook-Token: ${ORCA_AGENT_HOOK_TOKEN}" \\', diff --git a/src/main/copilot/copilot-managed-script.ts b/src/main/copilot/copilot-managed-script.ts index 6ee377054df..431b9a7a0d6 100644 --- a/src/main/copilot/copilot-managed-script.ts +++ b/src/main/copilot/copilot-managed-script.ts @@ -1,3 +1,4 @@ +import { posixCurlCommand } from '../agent-hooks/hook-post-command' import { buildWindowsAgentHookPostCommand, getSharedManagedScriptPath @@ -52,7 +53,7 @@ export function getManagedScript(target: 'local' | 'posix' = 'local'): string { ' exit 0', 'fi', // Keep large tool payloads off curl's command line to avoid EDR false positives. - 'printf \'%s\' "$payload" | curl -sS -X POST "http://127.0.0.1:${ORCA_AGENT_HOOK_PORT}/hook/copilot" \\', + `printf '%s' "$payload" | ${posixCurlCommand()} -sS -X POST "http://127.0.0.1:\${ORCA_AGENT_HOOK_PORT}/hook/copilot" \\`, ' --connect-timeout 0.5 --max-time 1.5 \\', ' -H "Content-Type: application/x-www-form-urlencoded" \\', ' -H "X-Orca-Agent-Hook-Token: ${ORCA_AGENT_HOOK_TOKEN}" \\', diff --git a/src/main/cursor/hook-script.ts b/src/main/cursor/hook-script.ts index 83d9be3d153..24908e9c52e 100644 --- a/src/main/cursor/hook-script.ts +++ b/src/main/cursor/hook-script.ts @@ -4,6 +4,7 @@ import { buildWindowsHookPowerShellCommand, wrapWindowsPowerShellEncodedCommand } from '../agent-hooks/installer-utils' +import { posixCurlCommand } from '../agent-hooks/hook-post-command' import { buildPosixHookPayloadCapture, buildPosixHookSpoolLines, @@ -83,7 +84,7 @@ export function getManagedScript(target: 'local' | 'posix' = 'local'): string { ' exit 0', 'fi', // Form encoding handles path-bearing IDs; stdin keeps large payloads off the command line. - 'printf \'%s\' "$payload" | curl -sS -X POST "http://127.0.0.1:${ORCA_AGENT_HOOK_PORT}/hook/cursor" \\', + `printf '%s' "$payload" | ${posixCurlCommand()} -sS -X POST "http://127.0.0.1:\${ORCA_AGENT_HOOK_PORT}/hook/cursor" \\`, ' --connect-timeout 0.5 --max-time 1.5 \\', ' -H "Content-Type: application/x-www-form-urlencoded" \\', ' -H "X-Orca-Agent-Hook-Token: ${ORCA_AGENT_HOOK_TOKEN}" \\', diff --git a/src/main/cursor/hook-service.test.ts b/src/main/cursor/hook-service.test.ts index 0869d35f8cf..f4c85ffc29a 100644 --- a/src/main/cursor/hook-service.test.ts +++ b/src/main/cursor/hook-service.test.ts @@ -135,7 +135,7 @@ describe('CursorHookService', () => { } else { // Stdin avoids oversized curl command lines that trigger EDR. expect(script).toContain(`payload=$(${POSIX_HOOK_STDIN_READER})`) - expect(script).toContain('printf \'%s\' "$payload" | curl') + expect(script).toContain('printf \'%s\' "$payload" | LC_ALL= LC_NUMERIC=C curl') expect(script).toContain('--data-urlencode "payload@-"') expect(script).not.toContain('--data-urlencode "payload=${payload}"') } diff --git a/src/main/devin/hook-service.test.ts b/src/main/devin/hook-service.test.ts index a9767fbba15..2a97d96532d 100644 --- a/src/main/devin/hook-service.test.ts +++ b/src/main/devin/hook-service.test.ts @@ -67,9 +67,11 @@ describe('DevinHookService', () => { const script = readFileSync(getDevinManagedScriptPath(), 'utf8') expect(script).toContain('/hook/devin') // stdin keeps large payloads off the command line to avoid EDR false positives. - expect(script).toContain('printf \'%s\' "$payload" | curl') expect(script).toContain('--data-urlencode "payload@-"') expect(script).not.toContain('--data-urlencode "payload=${payload}"') + if (process.platform !== 'win32') { + expect(script).toContain('printf \'%s\' "$payload" | LC_ALL= LC_NUMERIC=C curl') + } }) it('preserves unrelated keys in Devin config when installing hooks', () => { diff --git a/src/main/devin/hook-service.ts b/src/main/devin/hook-service.ts index ea0091c8664..08353dc4642 100644 --- a/src/main/devin/hook-service.ts +++ b/src/main/devin/hook-service.ts @@ -17,6 +17,7 @@ import { buildWindowsHookEnvironmentGuardLines, buildWindowsHookStdinDrainEpilogue } from '../agent-hooks/hook-stdin-contract' +import { posixCurlCommand } from '../agent-hooks/hook-post-command' import { applyDevinManagedHooks, DEVIN_EVENTS, @@ -68,7 +69,7 @@ function getManagedScript(target: 'local' | 'posix' = 'local'): string { 'fi', // Form encoding handles filesystem paths containing quotes or newlines. // stdin keeps large payloads off the command line to avoid EDR false positives. - 'printf \'%s\' "$payload" | curl -sS -X POST "http://127.0.0.1:${ORCA_AGENT_HOOK_PORT}/hook/devin" \\', + `printf '%s' "$payload" | ${posixCurlCommand()} -sS -X POST "http://127.0.0.1:\${ORCA_AGENT_HOOK_PORT}/hook/devin" \\`, ' --connect-timeout 0.5 --max-time 1.5 \\', ' -H "Content-Type: application/x-www-form-urlencoded" \\', ' -H "X-Orca-Agent-Hook-Token: ${ORCA_AGENT_HOOK_TOKEN}" \\', diff --git a/src/main/droid/hook-service.ts b/src/main/droid/hook-service.ts index bd496a29a42..9f0abf8e84a 100644 --- a/src/main/droid/hook-service.ts +++ b/src/main/droid/hook-service.ts @@ -28,6 +28,7 @@ import { buildWindowsHookEnvironmentGuardLines, buildWindowsHookStdinDrainEpilogue } from '../agent-hooks/hook-stdin-contract' +import { posixCurlCommand } from '../agent-hooks/hook-post-command' // SessionStart clears prompt/tool caches on resume before the next user prompt. const DROID_EVENTS = [ @@ -97,7 +98,7 @@ function getManagedScript(target: 'local' | 'posix' = 'local'): string { ' exit 0', 'fi', // Bound stalled posts and keep large payloads on stdin to avoid EDR command-line alerts. - 'printf \'%s\' "$payload" | curl -sS -X POST "http://127.0.0.1:${ORCA_AGENT_HOOK_PORT}/hook/droid" \\', + `printf '%s' "$payload" | ${posixCurlCommand()} -sS -X POST "http://127.0.0.1:\${ORCA_AGENT_HOOK_PORT}/hook/droid" \\`, ' --connect-timeout 0.5 --max-time 1.5 \\', ' -H "Content-Type: application/x-www-form-urlencoded" \\', ' -H "X-Orca-Agent-Hook-Token: ${ORCA_AGENT_HOOK_TOKEN}" \\', diff --git a/src/main/gemini/hook-service.ts b/src/main/gemini/hook-service.ts index a51fb0d066a..ee792c88a66 100644 --- a/src/main/gemini/hook-service.ts +++ b/src/main/gemini/hook-service.ts @@ -28,6 +28,7 @@ import { buildWindowsHookEnvironmentGuardLines, buildWindowsHookStdinDrainEpilogue } from '../agent-hooks/hook-stdin-contract' +import { posixCurlCommand } from '../agent-hooks/hook-post-command' // Gemini has no permission-prompt hook, so approvals cannot produce an Orca waiting state. // Gemini uses BeforeTool; remove stale Claude-style PreToolUse registrations below. @@ -83,7 +84,7 @@ function getManagedScript(target: 'local' | 'posix' = 'local'): string { ' exit 0', 'fi', // Form encoding handles quotes/newlines in paths; stdin keeps large payloads off the command line. - 'printf \'%s\' "$payload" | curl -sS -X POST "http://127.0.0.1:${ORCA_AGENT_HOOK_PORT}/hook/gemini" \\', + `printf '%s' "$payload" | ${posixCurlCommand()} -sS -X POST "http://127.0.0.1:\${ORCA_AGENT_HOOK_PORT}/hook/gemini" \\`, ' --connect-timeout 0.5 --max-time 1.5 \\', ' -H "Content-Type: application/x-www-form-urlencoded" \\', ' -H "X-Orca-Agent-Hook-Token: ${ORCA_AGENT_HOOK_TOKEN}" \\', diff --git a/src/main/grok/grok-hook-script.ts b/src/main/grok/grok-hook-script.ts index 2efb0ca006b..411e250960e 100644 --- a/src/main/grok/grok-hook-script.ts +++ b/src/main/grok/grok-hook-script.ts @@ -1,3 +1,4 @@ +import { posixCurlCommand } from '../agent-hooks/hook-post-command' import { getSharedManagedScriptPath, wrapPosixHookCommand } from '../agent-hooks/installer-utils' import { wrapWindowsBatchHookCommand } from '../agent-hooks/windows-direct-cmd-hook-command' import { @@ -45,7 +46,7 @@ export function getGrokManagedScript(target: 'local' | 'posix' = 'local'): strin `if [ -n "\${GROK_HOME:-}" ] && [ "\${#GROK_HOME}" -le ${GROK_HOME_ENVELOPE_MAX_LENGTH} ]; then`, ' grok_home=$GROK_HOME', 'fi', - 'printf \'%s\' "$payload" | curl -sS -X POST "http://127.0.0.1:${ORCA_AGENT_HOOK_PORT}/hook/grok" \\', + `printf '%s' "$payload" | ${posixCurlCommand()} -sS -X POST "http://127.0.0.1:\${ORCA_AGENT_HOOK_PORT}/hook/grok" \\`, ' --connect-timeout 0.5 --max-time 1.5 \\', ' -H "Content-Type: application/x-www-form-urlencoded" \\', ' -H "X-Orca-Agent-Hook-Token: ${ORCA_AGENT_HOOK_TOKEN}" \\', diff --git a/src/main/grok/hook-service.test.ts b/src/main/grok/hook-service.test.ts index 0afe93619c6..c4d7a1c4bcd 100644 --- a/src/main/grok/hook-service.test.ts +++ b/src/main/grok/hook-service.test.ts @@ -294,7 +294,7 @@ describe('GrokHookService', () => { script.indexOf(`payload=$(${POSIX_HOOK_JSON_STDIN_READER})`) ) expect(script).toContain(`payload=$(${POSIX_HOOK_JSON_STDIN_READER})`) - expect(script).toContain('printf \'%s\' "$payload" | curl') + expect(script).toContain('printf \'%s\' "$payload" | LC_ALL= LC_NUMERIC=C curl') expect(script).toContain('--data-urlencode "payload@-"') expect(script).toContain('${#GROK_HOME}" -le 4096') expect(script).toContain('--data-urlencode "grokHome=${grok_home}"') diff --git a/src/main/kimi/hook-service.test.ts b/src/main/kimi/hook-service.test.ts index e953014be5b..5d99cf4c3da 100644 --- a/src/main/kimi/hook-service.test.ts +++ b/src/main/kimi/hook-service.test.ts @@ -128,7 +128,7 @@ describe('KimiHookService', () => { const script = readFileSync(scriptPath(), 'utf-8') expect(script).toContain('/hook/kimi') // Keep large payloads off curl's command line to avoid EDR false positives. - expect(script).toContain('printf \'%s\' "$payload" | curl') + expect(script).toContain('printf \'%s\' "$payload" | LC_ALL= LC_NUMERIC=C curl') expect(script).toContain('--data-urlencode "payload@-"') expect(script).not.toContain('--data-urlencode "payload=${payload}"') expect(config).toContain('agent-hooks/kimi-hook.sh') diff --git a/src/main/kimi/hook-service.ts b/src/main/kimi/hook-service.ts index 35197a593a5..3cfb7ee6f7f 100644 --- a/src/main/kimi/hook-service.ts +++ b/src/main/kimi/hook-service.ts @@ -35,6 +35,7 @@ import { buildWindowsHookEnvironmentGuardLines, buildWindowsHookStdinDrainEpilogue } from '../agent-hooks/hook-stdin-contract' +import { posixCurlCommand } from '../agent-hooks/hook-post-command' import { applyManagedKimiHooks, KIMI_HOOK_EVENTS, @@ -98,7 +99,7 @@ function getManagedScript(target: 'local' | 'posix' = 'local'): string { ' exit 0', 'fi', // Form fields preserve arbitrary path text; stdin keeps large payloads off curl's command line for EDR. - 'printf \'%s\' "$payload" | curl -sS -X POST "http://127.0.0.1:${ORCA_AGENT_HOOK_PORT}/hook/kimi" \\', + `printf '%s' "$payload" | ${posixCurlCommand()} -sS -X POST "http://127.0.0.1:\${ORCA_AGENT_HOOK_PORT}/hook/kimi" \\`, ' --connect-timeout 0.5 --max-time 1.5 \\', ' -H "Content-Type: application/x-www-form-urlencoded" \\', ' -H "X-Orca-Agent-Hook-Token: ${ORCA_AGENT_HOOK_TOKEN}" \\', diff --git a/src/main/muse/hook-service.test.ts b/src/main/muse/hook-service.test.ts index f1a89ecc828..58e9c389d4d 100644 --- a/src/main/muse/hook-service.test.ts +++ b/src/main/muse/hook-service.test.ts @@ -76,7 +76,9 @@ describe('MuseHookService', () => { expect(script).toContain('/hook/muse') // Stdin payloads avoid oversized command-line EDR signals. expect(script).toContain( - process.platform === 'win32' ? 'payload@-' : 'printf \'%s\' "$payload" | curl' + process.platform === 'win32' + ? 'payload@-' + : 'printf \'%s\' "$payload" | LC_ALL= LC_NUMERIC=C curl' ) })