mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
fix(grok): stop replayed Claude/Cursor hooks reporting Grok panes as Claude (#20507)
* fix(grok): stop replayed Claude/Cursor hooks reporting Grok panes as Claude Grok's hook discovery reads ~/.claude/settings.json (and the Cursor equivalent) for vendor compatibility, and that is on by default. So inside every Grok pane Orca's managed Claude hook fires in addition to Orca's managed Grok hook, and both POST the same Grok envelope. The Claude-routed copy lands last and wins, so the pane's agent type is resolved from the POST route as "claude" and no Grok-specific normalization runs for it. Guard the managed Claude and Cursor scripts on GROK_HOOK_EVENT, which Grok's hook runner stamps into every hook subprocess it spawns — including replayed vendor configs — after any user-supplied environment, so a hook cannot spoof it. This mirrors the existing DEVIN_PROJECT_DIR guard in the same script, which solves the identical problem for another agent that imports Claude hooks. Placement is load-bearing: the guard sits after the stdin capture, so Grok's writer never blocks, and before both the spool write and the HTTP POST, so a replayed event cannot leave a spool entry that replays later. The Windows variants jump to the stdin-drain label rather than exiting, because abandoning stdin there hangs the writer. The guard is scoped to agent === 'claude'; OpenClaude reuses ClaudeHookService with its own settings file, which Grok does not replay, so it is unaffected. Verified live against Grok 1.0.25 in a dev instance: the pane's reported agent type goes from "claude" to "grok" on every turn-end, including the hidden follow-up turns Grok runs when background work finishes. The guard pushed hook-service.ts past the 300-line cap, so the script builder moves to a sibling hook-script.ts. That mirrors the existing split under src/main/cursor/, where the service owns install/status and the script module owns script text. * fix(agent-hooks): preserve Windows background worker stdin contract
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
"../src/cli/**/*",
|
||||
"../src/shared/**/*",
|
||||
"../src/main/agent-state-file-reader.ts",
|
||||
"../src/main/agent-hooks/grok-replay-guard.ts",
|
||||
"../src/main/claude/hook-script.ts",
|
||||
"../src/main/agent-hooks/hook-stdin-contract.ts",
|
||||
"../src/main/agent-hooks/hook-post-command.ts",
|
||||
"../src/main/agent-hooks/hook-config-write-path.ts",
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { chmodSync, existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: () => '/tmp/userData'
|
||||
}
|
||||
}))
|
||||
|
||||
import { getManagedScript as getClaudeManagedScript } from '../claude/hook-service'
|
||||
import { getManagedScript as getCursorManagedScript } from '../cursor/hook-script'
|
||||
|
||||
const POSIX_GROK_GUARD = 'if [ -n "$GROK_HOOK_EVENT" ]; then'
|
||||
const WINDOWS_GROK_GUARD = 'if not "%GROK_HOOK_EVENT%"=="" goto :orca_agent_hook_drain_stdin'
|
||||
const CLAUDE_SCRIPT_OPTIONS = {
|
||||
skipWhenDevinImportsClaude: true,
|
||||
skipWhenGrokImportsClaude: true
|
||||
}
|
||||
|
||||
function withPlatform<T>(platform: NodeJS.Platform, run: () => T): T {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(process, 'platform')!
|
||||
Object.defineProperty(process, 'platform', { value: platform, configurable: true })
|
||||
try {
|
||||
return run()
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
function expectGuardBeforeTransport(
|
||||
script: string,
|
||||
guard: string,
|
||||
response: string,
|
||||
spool?: string
|
||||
): void {
|
||||
const guardIndex = script.indexOf(guard)
|
||||
expect(guardIndex).toBeGreaterThan(script.indexOf(response))
|
||||
expect(guardIndex).toBeLessThan(script.indexOf('curl'))
|
||||
if (spool) {
|
||||
expect(guardIndex).toBeLessThan(script.indexOf(spool))
|
||||
}
|
||||
}
|
||||
|
||||
function runPosixHook(
|
||||
script: string,
|
||||
grokHookEvent: string
|
||||
): {
|
||||
curlCalled: boolean
|
||||
stdout: string
|
||||
} {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'orca-grok-replay-'))
|
||||
const scriptPath = join(dir, 'hook.sh')
|
||||
const curlPath = join(dir, 'curl')
|
||||
const curlLog = join(dir, 'curl.log')
|
||||
try {
|
||||
writeFileSync(scriptPath, script)
|
||||
writeFileSync(
|
||||
curlPath,
|
||||
'#!/bin/sh\n{ command -p cat 2>/dev/null || cat; } >/dev/null\nprintf "called\\n" >> "$CURL_LOG"\n'
|
||||
)
|
||||
chmodSync(scriptPath, 0o755)
|
||||
chmodSync(curlPath, 0o755)
|
||||
|
||||
const result = spawnSync('/bin/sh', [scriptPath], {
|
||||
encoding: 'utf8',
|
||||
input: '{"hook_event_name":"Stop"}',
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${dir}:${process.env.PATH ?? ''}`,
|
||||
CURL_LOG: curlLog,
|
||||
GROK_HOOK_EVENT: grokHookEvent,
|
||||
ORCA_AGENT_HOOK_ENDPOINT: '',
|
||||
ORCA_AGENT_HOOK_PORT: '1234',
|
||||
ORCA_AGENT_HOOK_TOKEN: 'token',
|
||||
ORCA_PANE_KEY: 'tab:leaf'
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.status).toBe(0)
|
||||
return { curlCalled: existsSync(curlLog), stdout: result.stdout }
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
describe('Grok vendor hook replay guard', () => {
|
||||
it('precedes spooling and HTTP in the generated POSIX Claude and Cursor scripts', () => {
|
||||
const claude = getClaudeManagedScript('posix', CLAUDE_SCRIPT_OPTIONS)
|
||||
const cursor = getCursorManagedScript('posix')
|
||||
|
||||
expectGuardBeforeTransport(claude, POSIX_GROK_GUARD, 'printf "{}\\n"', 'spool_hook_event')
|
||||
expectGuardBeforeTransport(cursor, POSIX_GROK_GUARD, 'printf "{}\\n"', 'spool_hook_event')
|
||||
})
|
||||
|
||||
it('precedes HTTP while preserving fail-open output in generated Windows scripts', () => {
|
||||
const { claude, cursor } = withPlatform('win32', () => ({
|
||||
claude: getClaudeManagedScript('local', CLAUDE_SCRIPT_OPTIONS),
|
||||
cursor: getCursorManagedScript('local')
|
||||
}))
|
||||
|
||||
expectGuardBeforeTransport(claude, WINDOWS_GROK_GUARD, 'echo {}')
|
||||
expectGuardBeforeTransport(cursor, WINDOWS_GROK_GUARD, '(echo {})')
|
||||
const backgroundWorkerGuardIndex = claude.indexOf('CLAUDE_JOB_DIR')
|
||||
expect(backgroundWorkerGuardIndex).toBeGreaterThan(-1)
|
||||
expect(backgroundWorkerGuardIndex).toBeLessThan(claude.indexOf(WINDOWS_GROK_GUARD))
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'drops Grok-replayed hooks without suppressing their protocol response',
|
||||
() => {
|
||||
for (const script of [
|
||||
getClaudeManagedScript('posix', CLAUDE_SCRIPT_OPTIONS),
|
||||
getCursorManagedScript('posix')
|
||||
]) {
|
||||
const result = runPosixHook(script, 'Stop')
|
||||
expect(result.curlCalled).toBe(false)
|
||||
expect(result.stdout).toBe('{}\n')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it.skipIf(process.platform === 'win32')('leaves non-Grok hook delivery unchanged', () => {
|
||||
for (const script of [
|
||||
getClaudeManagedScript('posix', CLAUDE_SCRIPT_OPTIONS),
|
||||
getCursorManagedScript('posix')
|
||||
]) {
|
||||
const result = runPosixHook(script, '')
|
||||
expect(result.curlCalled).toBe(true)
|
||||
expect(result.stdout).toBe('{}\n')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { WINDOWS_HOOK_STDIN_DRAIN_LABEL } from './hook-stdin-contract'
|
||||
|
||||
export function buildPosixGrokReplayGuardLines(): string[] {
|
||||
return [
|
||||
// Why: Grok imports vendor hooks; only its native hook may report the event as Grok.
|
||||
'if [ -n "$GROK_HOOK_EVENT" ]; then',
|
||||
' exit 0',
|
||||
'fi'
|
||||
]
|
||||
}
|
||||
|
||||
export function buildWindowsGrokReplayGuardLines(): string[] {
|
||||
return [`if not "%GROK_HOOK_EVENT%"=="" goto :${WINDOWS_HOOK_STDIN_DRAIN_LABEL}`]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/** The managed Claude-compatible hook script, built for local, POSIX-remote and Windows targets.
|
||||
* Split from hook-service.ts so the service owns install/status and this owns script text,
|
||||
* mirroring the same split under src/main/cursor/. */
|
||||
import { buildWindowsAgentHookCurlPostCommand } from '../agent-hooks/installer-utils'
|
||||
import { buildPosixAgentHookPostCommand } from '../agent-hooks/hook-post-command'
|
||||
import {
|
||||
buildPosixGrokReplayGuardLines,
|
||||
buildWindowsGrokReplayGuardLines
|
||||
} from '../agent-hooks/grok-replay-guard'
|
||||
import {
|
||||
WINDOWS_HOOK_STDIN_DRAIN_LABEL,
|
||||
buildPosixHookPayloadCapture,
|
||||
buildPosixHookSpoolLines,
|
||||
buildWindowsHookEnvironmentGuardLines,
|
||||
buildWindowsHookStdinDrainEpilogue
|
||||
} from '../agent-hooks/hook-stdin-contract'
|
||||
|
||||
export function getManagedScript(
|
||||
target: 'local' | 'posix' = 'local',
|
||||
options: {
|
||||
skipWhenDevinImportsClaude?: boolean
|
||||
skipWhenGrokImportsClaude?: boolean
|
||||
} = {}
|
||||
): string {
|
||||
if (target === 'local' && process.platform === 'win32') {
|
||||
return [
|
||||
'@echo off',
|
||||
'setlocal',
|
||||
// Why: Claude-compatible permission hooks fail closed on empty stdout (#14818).
|
||||
'echo {}',
|
||||
// Why: refresh endpoint coordinates for PTYs surviving an Orca restart.
|
||||
'if defined ORCA_AGENT_HOOK_ENDPOINT if exist "%ORCA_AGENT_HOOK_ENDPOINT%" call "%ORCA_AGENT_HOOK_ENDPOINT%" 2>nul',
|
||||
// Why (#11549): the env guards must outrank the Devin skip — the Devin skip parks in more.com,
|
||||
// and outside an Orca pane the caller can abandon stdin, so more.com never returns.
|
||||
...buildWindowsHookEnvironmentGuardLines(),
|
||||
// Why: a backgrounded session runs in a daemon worker that inherited the dispatching
|
||||
// pane's env, so ORCA_PANE_KEY names a pane this session does not run in (#9236).
|
||||
// Why exit, not the drain label: the drain parks in more.com and a worker is outside
|
||||
// an Orca pane — the abandoned-stdin hang #11549 guards against.
|
||||
'if not "%CLAUDE_JOB_DIR%"=="" exit /b 0',
|
||||
...(options.skipWhenGrokImportsClaude ? buildWindowsGrokReplayGuardLines() : []),
|
||||
...(options.skipWhenDevinImportsClaude
|
||||
? [
|
||||
// Why: Devin imports .claude hooks by default; skip Orca's managed hook there so status posts stay attributed to Devin.
|
||||
`if not "%DEVIN_PROJECT_DIR%"=="" goto :${WINDOWS_HOOK_STDIN_DRAIN_LABEL}`
|
||||
]
|
||||
: []),
|
||||
// Why: use curl.exe to avoid an extra PowerShell startup per hook.
|
||||
buildWindowsAgentHookCurlPostCommand('claude'),
|
||||
'exit /b 0',
|
||||
...buildWindowsHookStdinDrainEpilogue(),
|
||||
''
|
||||
].join('\r\n')
|
||||
}
|
||||
|
||||
return [
|
||||
'#!/bin/sh',
|
||||
// Why: Claude-compatible permission hooks fail closed on empty stdout (#14818).
|
||||
'printf "{}\\n"',
|
||||
...buildPosixHookPayloadCapture(),
|
||||
...(options.skipWhenGrokImportsClaude ? buildPosixGrokReplayGuardLines() : []),
|
||||
...buildPosixHookSpoolLines('claude'),
|
||||
...(options.skipWhenDevinImportsClaude
|
||||
? [
|
||||
// Why: Devin imports .claude hooks by default; skip Orca's managed hook there so status posts stay attributed to Devin.
|
||||
'if [ -n "$DEVIN_PROJECT_DIR" ]; then',
|
||||
' exit 0',
|
||||
'fi'
|
||||
]
|
||||
: []),
|
||||
// Why: a backgrounded session runs in a daemon worker that inherited the dispatching
|
||||
// pane's env, so ORCA_PANE_KEY names a pane this session does not run in (#9236).
|
||||
'if [ -n "$CLAUDE_JOB_DIR" ]; then',
|
||||
' exit 0',
|
||||
'fi',
|
||||
// Why: refresh endpoint coordinates for PTYs surviving an Orca restart.
|
||||
// Why: suppress parse errors so they neither leak nor trip outer set -e.
|
||||
'if [ -n "$ORCA_AGENT_HOOK_ENDPOINT" ] && [ -r "$ORCA_AGENT_HOOK_ENDPOINT" ]; then',
|
||||
' unset ORCA_AGENT_HOOK_TRANSPORT',
|
||||
' . "$ORCA_AGENT_HOOK_ENDPOINT" 2>/dev/null || :',
|
||||
'fi',
|
||||
'if [ -z "$ORCA_AGENT_HOOK_PORT" ] || [ -z "$ORCA_AGENT_HOOK_TOKEN" ] || [ -z "$ORCA_PANE_KEY" ]; then',
|
||||
' spool_hook_event',
|
||||
' exit 0',
|
||||
'fi',
|
||||
// Why: keep full hook JSON off the command line and avoid IDS-friendly URL-encoded paths.
|
||||
...buildPosixAgentHookPostCommand('claude').map((line, index, lines) =>
|
||||
index === lines.length - 1 ? `${line} >/dev/null 2>&1 || spool_hook_event` : line
|
||||
),
|
||||
'exit 0',
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
@@ -262,6 +262,7 @@ describe('ClaudeHookService.install', () => {
|
||||
'utf-8'
|
||||
)
|
||||
expect(managedScript).toContain('DEVIN_PROJECT_DIR')
|
||||
expect(managedScript).toContain('GROK_HOOK_EVENT')
|
||||
// Why: guard and Devin-skip paths must still return neutral JSON (#14818).
|
||||
expect(managedScript).toMatch(
|
||||
process.platform === 'win32'
|
||||
@@ -711,6 +712,7 @@ describe('ClaudeHookService.installRemote', () => {
|
||||
const script = fs.files.get('/home/dev/.orca/agent-hooks/claude-hook.sh')
|
||||
expect(script).toContain('#!/bin/sh')
|
||||
expect(script).toContain('DEVIN_PROJECT_DIR')
|
||||
expect(script).toContain('GROK_HOOK_EVENT')
|
||||
// Why: remote guard paths must still return neutral JSON (#14818).
|
||||
expect(script!.indexOf('printf "{}\\n"')).toBe(
|
||||
script!.indexOf('#!/bin/sh') + '#!/bin/sh\n'.length
|
||||
@@ -813,6 +815,9 @@ describe('OpenClaudeHookService-compatible install', () => {
|
||||
expect(
|
||||
readFileSync(join(tmpHome, '.orca', 'agent-hooks', OPENCLAUDE_SCRIPT_FILE_NAME), 'utf-8')
|
||||
).not.toContain('DEVIN_PROJECT_DIR')
|
||||
expect(
|
||||
readFileSync(join(tmpHome, '.orca', 'agent-hooks', OPENCLAUDE_SCRIPT_FILE_NAME), 'utf-8')
|
||||
).not.toContain('GROK_HOOK_EVENT')
|
||||
// Why: the statusline usage feed is Claude-only; OpenClaude installs must not set statusLine.
|
||||
expect(parsed.statusLine).toBeUndefined()
|
||||
expect(existsSync(join(tmpHome, '.claude', 'settings.json'))).toBe(false)
|
||||
|
||||
@@ -3,26 +3,20 @@ import type { SFTPWrapper } from 'ssh2'
|
||||
import type { AgentHookInstallState, AgentHookInstallStatus } from '../../shared/agent-hook-types'
|
||||
import {
|
||||
buildManagedCommandHook,
|
||||
buildWindowsAgentHookCurlPostCommand,
|
||||
readHooksJson,
|
||||
writeHooksJson,
|
||||
writeManagedScript,
|
||||
type HooksConfig
|
||||
type HooksConfig,
|
||||
writeManagedScript
|
||||
} from '../agent-hooks/installer-utils'
|
||||
import { buildPosixAgentHookPostCommand } from '../agent-hooks/hook-post-command'
|
||||
import {
|
||||
readHooksJsonRemote,
|
||||
writeHooksJsonRemote,
|
||||
writeManagedScriptRemote
|
||||
} from '../agent-hooks/installer-utils-remote'
|
||||
import { refreshManagedScriptIfPresent } from '../agent-hooks/managed-hook-script-refresh'
|
||||
import {
|
||||
buildPosixHookPayloadCapture,
|
||||
buildPosixHookSpoolLines,
|
||||
buildWindowsHookEnvironmentGuardLines,
|
||||
buildWindowsHookStdinDrainEpilogue,
|
||||
WINDOWS_HOOK_STDIN_DRAIN_LABEL
|
||||
} from '../agent-hooks/hook-stdin-contract'
|
||||
import { getManagedScript } from './hook-script'
|
||||
|
||||
export { getManagedScript }
|
||||
import { getManagedStatusLineScript } from './statusline-script'
|
||||
import {
|
||||
applyManagedHooks,
|
||||
@@ -59,78 +53,6 @@ const DEFAULT_CLAUDE_HOOK_SERVICE_OPTIONS: ClaudeHookServiceOptions = {
|
||||
settings: CLAUDE_HOOK_SETTINGS
|
||||
}
|
||||
|
||||
function getManagedScript(
|
||||
target: 'local' | 'posix' = 'local',
|
||||
options: { skipWhenDevinImportsClaude?: boolean } = {}
|
||||
): string {
|
||||
if (target === 'local' && process.platform === 'win32') {
|
||||
return [
|
||||
'@echo off',
|
||||
'setlocal',
|
||||
// Why: Claude-compatible permission hooks fail closed on empty stdout (#14818).
|
||||
'echo {}',
|
||||
// Why: refresh endpoint coordinates for PTYs surviving an Orca restart.
|
||||
'if defined ORCA_AGENT_HOOK_ENDPOINT if exist "%ORCA_AGENT_HOOK_ENDPOINT%" call "%ORCA_AGENT_HOOK_ENDPOINT%" 2>nul',
|
||||
// Why (#11549): the env guards must outrank the Devin skip — the Devin skip parks in more.com,
|
||||
// and outside an Orca pane the caller can abandon stdin, so more.com never returns.
|
||||
...buildWindowsHookEnvironmentGuardLines(),
|
||||
// Why: a backgrounded session runs in a daemon worker that inherited the dispatching
|
||||
// pane's env, so ORCA_PANE_KEY names a pane this session does not run in (#9236).
|
||||
// Why exit, not the drain label: the drain parks in more.com and a worker is outside
|
||||
// an Orca pane — the abandoned-stdin hang #11549 guards against.
|
||||
'if not "%CLAUDE_JOB_DIR%"=="" exit /b 0',
|
||||
...(options.skipWhenDevinImportsClaude
|
||||
? [
|
||||
// Why: Devin imports .claude hooks by default; skip Orca's managed hook there so status posts stay attributed to Devin.
|
||||
`if not "%DEVIN_PROJECT_DIR%"=="" goto :${WINDOWS_HOOK_STDIN_DRAIN_LABEL}`
|
||||
]
|
||||
: []),
|
||||
// Why: use curl.exe to avoid an extra PowerShell startup per hook.
|
||||
buildWindowsAgentHookCurlPostCommand('claude'),
|
||||
'exit /b 0',
|
||||
...buildWindowsHookStdinDrainEpilogue(),
|
||||
''
|
||||
].join('\r\n')
|
||||
}
|
||||
|
||||
return [
|
||||
'#!/bin/sh',
|
||||
// Why: Claude-compatible permission hooks fail closed on empty stdout (#14818).
|
||||
'printf "{}\\n"',
|
||||
...buildPosixHookPayloadCapture(),
|
||||
...buildPosixHookSpoolLines('claude'),
|
||||
...(options.skipWhenDevinImportsClaude
|
||||
? [
|
||||
// Why: Devin imports .claude hooks by default; skip Orca's managed hook there so status posts stay attributed to Devin.
|
||||
'if [ -n "$DEVIN_PROJECT_DIR" ]; then',
|
||||
' exit 0',
|
||||
'fi'
|
||||
]
|
||||
: []),
|
||||
// Why: a backgrounded session runs in a daemon worker that inherited the dispatching
|
||||
// pane's env, so ORCA_PANE_KEY names a pane this session does not run in (#9236).
|
||||
'if [ -n "$CLAUDE_JOB_DIR" ]; then',
|
||||
' exit 0',
|
||||
'fi',
|
||||
// Why: refresh endpoint coordinates for PTYs surviving an Orca restart.
|
||||
// Why: suppress parse errors so they neither leak nor trip outer set -e.
|
||||
'if [ -n "$ORCA_AGENT_HOOK_ENDPOINT" ] && [ -r "$ORCA_AGENT_HOOK_ENDPOINT" ]; then',
|
||||
' unset ORCA_AGENT_HOOK_TRANSPORT',
|
||||
' . "$ORCA_AGENT_HOOK_ENDPOINT" 2>/dev/null || :',
|
||||
'fi',
|
||||
'if [ -z "$ORCA_AGENT_HOOK_PORT" ] || [ -z "$ORCA_AGENT_HOOK_TOKEN" ] || [ -z "$ORCA_PANE_KEY" ]; then',
|
||||
' spool_hook_event',
|
||||
' exit 0',
|
||||
'fi',
|
||||
// Why: keep full hook JSON off the command line and avoid IDS-friendly URL-encoded paths.
|
||||
...buildPosixAgentHookPostCommand('claude').map((line, index, lines) =>
|
||||
index === lines.length - 1 ? `${line} >/dev/null 2>&1 || spool_hook_event` : line
|
||||
),
|
||||
'exit 0',
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export class ClaudeHookService {
|
||||
private readonly options: ClaudeHookServiceOptions
|
||||
|
||||
@@ -188,7 +110,10 @@ export class ClaudeHookService {
|
||||
async refreshManagedScripts(): Promise<void> {
|
||||
await refreshManagedScriptIfPresent(
|
||||
getManagedScriptPath(this.options.settings),
|
||||
getManagedScript('local', { skipWhenDevinImportsClaude: this.options.agent === 'claude' })
|
||||
getManagedScript('local', {
|
||||
skipWhenDevinImportsClaude: this.options.agent === 'claude',
|
||||
skipWhenGrokImportsClaude: this.options.agent === 'claude'
|
||||
})
|
||||
)
|
||||
// Why: no agent gate — the statusline script only ever exists for claude, so presence is the gate.
|
||||
await refreshManagedScriptIfPresent(
|
||||
@@ -219,7 +144,10 @@ export class ClaudeHookService {
|
||||
)
|
||||
writeManagedScript(
|
||||
scriptPath,
|
||||
getManagedScript('local', { skipWhenDevinImportsClaude: this.options.agent === 'claude' })
|
||||
getManagedScript('local', {
|
||||
skipWhenDevinImportsClaude: this.options.agent === 'claude',
|
||||
skipWhenGrokImportsClaude: this.options.agent === 'claude'
|
||||
})
|
||||
)
|
||||
// Why: the statusline usage feed is Claude-only — OpenClaude data would be misattributed to the Claude provider.
|
||||
if (this.options.agent === 'claude') {
|
||||
@@ -281,7 +209,10 @@ export class ClaudeHookService {
|
||||
await writeManagedScriptRemote(
|
||||
sftp,
|
||||
remoteScriptPath,
|
||||
getManagedScript('posix', { skipWhenDevinImportsClaude: this.options.agent === 'claude' })
|
||||
getManagedScript('posix', {
|
||||
skipWhenDevinImportsClaude: this.options.agent === 'claude',
|
||||
skipWhenGrokImportsClaude: this.options.agent === 'claude'
|
||||
})
|
||||
)
|
||||
// Why: no statusline install here — this path serves SSH remotes and WSL guests, whose relay hook
|
||||
// listener doesn't route /statusline/claude, and an SSH box's Claude login can be a different
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
buildWindowsHookEnvironmentGuardLines,
|
||||
buildWindowsHookStdinDrainEpilogue
|
||||
} from '../agent-hooks/hook-stdin-contract'
|
||||
import {
|
||||
buildPosixGrokReplayGuardLines,
|
||||
buildWindowsGrokReplayGuardLines
|
||||
} from '../agent-hooks/grok-replay-guard'
|
||||
import { getCursorHookResponse, type CursorEvent } from './hook-events'
|
||||
|
||||
const CURSOR_HOOK_RESPONSE_ENV = 'ORCA_CURSOR_HOOK_RESPONSE'
|
||||
@@ -43,6 +47,7 @@ export function getManagedScript(target: 'local' | 'posix' = 'local'): string {
|
||||
// Why: source current endpoint coordinates for PTYs surviving an Orca restart.
|
||||
'if defined ORCA_AGENT_HOOK_ENDPOINT if exist "%ORCA_AGENT_HOOK_ENDPOINT%" call "%ORCA_AGENT_HOOK_ENDPOINT%" 2>nul',
|
||||
...buildWindowsHookEnvironmentGuardLines(),
|
||||
...buildWindowsGrokReplayGuardLines(),
|
||||
buildWindowsAgentHookPostCommand('cursor'),
|
||||
'exit /b 0',
|
||||
...buildWindowsHookStdinDrainEpilogue(),
|
||||
@@ -59,6 +64,7 @@ export function getManagedScript(target: 'local' | 'posix' = 'local'): string {
|
||||
' printf "{}\\n"',
|
||||
'fi',
|
||||
...buildPosixHookPayloadCapture(),
|
||||
...buildPosixGrokReplayGuardLines(),
|
||||
...buildPosixHookSpoolLines('cursor'),
|
||||
// Why: refresh endpoint coordinates so surviving PTYs keep reporting.
|
||||
'if [ -n "$ORCA_AGENT_HOOK_ENDPOINT" ] && [ -r "$ORCA_AGENT_HOOK_ENDPOINT" ]; then',
|
||||
|
||||
@@ -131,6 +131,7 @@ describe('CursorHookService', () => {
|
||||
'utf8'
|
||||
)
|
||||
expect(script).toContain('/hook/cursor')
|
||||
expect(script).toContain('GROK_HOOK_EVENT')
|
||||
if (process.platform === 'win32') {
|
||||
expect(script).toContain('%SystemRoot%\\System32\\curl.exe')
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user