fix(grok): stop SessionStart orca-status hook hanging for 10s (#20090)

* fix(grok): stop SessionStart orca-status hook hanging for 10s

Grok writes one JSON hook payload and waits for the process to exit
without closing stdin. The POSIX hook used `cat`, which waits for EOF,
so SessionStart deadlocked until Grok's 10s timeout:

  session_start hook (global/orca-status) failed, ignored: timed out after 10000ms

Read one JSON object with raw_decode instead; return as soon as the
object is complete. Fall back to cat when Python is missing.

* fix(grok): decode the hook payload incrementally instead of per chunk

The JSON stdin reader strict-decoded the whole accumulated buffer after
every read and caught only json.JSONDecodeError. A multi-byte character
split across two os.read calls therefore raised UnicodeDecodeError, which
is a ValueError but not a JSONDecodeError, so the interpreter died — after
consuming stdin. The `python3 || python || cat` chain then handed the next
reader a truncated stream, and `cat` blocked on a pipe the caller never
closes, reinstating the exact 10s SessionStart timeout this reader exists
to avoid. Measured: a CJK+emoji payload written byte by byte hung until it
was killed; a 200KB payload split mid-character arrived as 4 bytes.

Hold the decoder across reads, treat any ValueError as "not complete yet",
and guard the whole program so a non-zero exit implies stdin was never
read — only then is the `||` fallback safe. Emit the object's own text
rather than re-serialising it, which was rewriting non-ASCII as \uXXXX.
Skip leading whitespace, which raw_decode does not. Separate the
first-byte wait (5s) from the idle wait (1.5s) so a writer that is merely
late is no longer dropped.

Also unset a HOME that does not exist before spawning the interpreter:
macOS resolves /usr/bin/python3 through an Xcode stub that re-runs its
whole tool lookup without a reachable cache, costing 6.6s per spawn and
overrunning Grok's budget on its own. This is what made the existing
large-payload and empty-PATH lifecycle cases fail.

The Python program now lives in a shell variable instead of being inlined
twice, which halves the generated script and keeps it readable.

---------

Co-authored-by: Neil <neil@stably.ai>
(cherry picked from commit 01f8aa8d96)
This commit is contained in:
Bjorn Runaker
2026-09-14 21:52:21 -07:00
committed by Neil
parent d0df6b8ed0
commit 54eaa14756
6 changed files with 483 additions and 7 deletions
+98 -2
View File
@@ -7,15 +7,111 @@ export type PosixHookEmptyPayloadPolicy = 'exit' | 'empty-object'
export const POSIX_HOOK_STDIN_READER = '{ command -p cat 2>/dev/null || cat; }'
export const POSIX_HOOK_STDIN_DRAIN_COMMAND = `${POSIX_HOOK_STDIN_READER} >/dev/null 2>&1 || :`
/** Seconds the JSON reader waits for the writer's first byte before giving up.
* Comfortably inside Grok's 10s hook timeout, and far enough above process
* startup that a loaded or remote host cannot lose a payload that is merely late. */
export const POSIX_HOOK_JSON_STDIN_FIRST_BYTE_TIMEOUT_SECONDS = 5
/** Seconds of silence that end a payload which never parses as JSON (the `cat` shape). */
export const POSIX_HOOK_JSON_STDIN_IDLE_TIMEOUT_SECONDS = 1.5
// Why: Grok SessionStart writes one JSON object and then waits for the hook to
// exit without closing stdin, so reading to EOF deadlocks until Grok's 10s
// timeout. Return as soon as the first complete JSON value has arrived.
//
// Three invariants this script must hold, because the shell chains a second
// reader behind it and a reader that consumed bytes cannot be retried:
// 1. A non-zero exit implies stdin was never read, so the `||` fallback still
// sees the whole stream. Everything after the imports is therefore guarded.
// 2. Decoding is incremental. A multi-byte character straddling two reads must
// not raise, or a CJK/emoji payload falls through to `cat` and hangs.
// 3. The payload is emitted unchanged. Re-serialising would rewrite non-ASCII
// as \uXXXX and reorder keys behind the agent's back.
const POSIX_HOOK_JSON_STDIN_PYTHON = [
'import codecs, json, os, select',
'text = ""',
'try:',
' decoder = codecs.getincrementaldecoder("utf-8")("replace")',
` timeout = ${POSIX_HOOK_JSON_STDIN_FIRST_BYTE_TIMEOUT_SECONDS}.0`,
' while 1:',
' if not select.select([0], [], [], timeout)[0]:',
' text += decoder.decode(b"", True)',
' break',
' chunk = os.read(0, 65536)',
' if not chunk:',
' text += decoder.decode(b"", True)',
' break',
` timeout = ${POSIX_HOOK_JSON_STDIN_IDLE_TIMEOUT_SECONDS}`,
' text += decoder.decode(chunk)',
// raw_decode does not skip leading whitespace, so a padded payload would
// otherwise never complete and would wait out the idle timeout.
' value = text.lstrip()',
' if not value:',
' continue',
' try:',
' end = json.JSONDecoder().raw_decode(value)[1]',
' except ValueError:',
' continue',
' text = value[:end]',
' break',
'except Exception:',
' pass',
'try:',
// os.write skips the locale-dependent stdout encoder, which raises under
// LC_ALL=C for a non-ASCII payload.
' data = text.encode("utf-8")',
' written = 0',
' while written < len(data):',
' written += os.write(1, data[written:])',
'except Exception:',
' pass'
].join('\n')
// Why a variable rather than two inline copies: the script is embedded twice in
// the reader chain, and `-c '<600 chars>'` twice is an EDR oversized-command-line
// signal as well as unreadable in the generated hook.
const POSIX_HOOK_JSON_STDIN_PYTHON_VAR = 'orca_hook_json_stdin_py'
export const POSIX_HOOK_JSON_STDIN_PRELUDE: readonly string[] = [
`${POSIX_HOOK_JSON_STDIN_PYTHON_VAR}='${POSIX_HOOK_JSON_STDIN_PYTHON}'`
]
const jsonStdinInterpreter = (name: string): string =>
`command -p ${name} -c "$${POSIX_HOOK_JSON_STDIN_PYTHON_VAR}" 2>/dev/null`
// Why: macOS ships /usr/bin/python3 as an Xcode stub that re-resolves the real
// interpreter on every run when it cannot reach its cache under $HOME. A HOME
// that does not exist costs ~6.6s per spawn there, which alone overruns Grok's
// 10s hook budget. Unsetting it brings that back to ~95ms and is what a
// home-less process sees anyway. Safe to mutate: the reader only ever runs
// inside the `payload=$(...)` subshell, so the hook's own HOME is untouched.
const POSIX_HOOK_JSON_STDIN_HOME_GUARD = '{ [ -d "${HOME:-}" ] || unset HOME; }'
// Why `python` too: the script avoids py3-only syntax (verified on 2.7) so a host
// that only ships `python` does not drop straight to the `cat` hang.
export const POSIX_HOOK_JSON_STDIN_READER = `${POSIX_HOOK_JSON_STDIN_HOME_GUARD}; ${jsonStdinInterpreter('python3')} || ${jsonStdinInterpreter('python')} || ${POSIX_HOOK_STDIN_READER}`
/** Optional reader override for an agent whose caller keeps stdin open after the payload.
* `prelude` must be emitted before the capture line; keep them together. */
export type PosixHookStdinReader = {
readonly reader: string
readonly prelude: readonly string[]
}
export const POSIX_HOOK_JSON_STDIN: PosixHookStdinReader = {
reader: POSIX_HOOK_JSON_STDIN_READER,
prelude: POSIX_HOOK_JSON_STDIN_PRELUDE
}
// Why: every POSIX hook must own stdin before any no-op exit; sharing this
// prelude prevents agent templates from inventing different drain semantics.
export function buildPosixHookPayloadCapture(
emptyPayloadPolicy: PosixHookEmptyPayloadPolicy = 'exit'
emptyPayloadPolicy: PosixHookEmptyPayloadPolicy = 'exit',
stdinReader: PosixHookStdinReader = { reader: POSIX_HOOK_STDIN_READER, prelude: [] }
): string[] {
const emptyPayloadLines =
emptyPayloadPolicy === 'empty-object' ? [" payload='{}'"] : [' exit 0']
return [
`payload=$(${POSIX_HOOK_STDIN_READER})`,
...stdinReader.prelude,
`payload=$(${stdinReader.reader})`,
'if [ -z "$payload" ]; then',
...emptyPayloadLines,
'fi'
@@ -64,6 +64,8 @@ import { KimiHookService } from '../kimi/hook-service'
import { openClaudeHookService } from '../openclaude/hook-service'
import { wrapPosixHookCommand, wrapWindowsHookCommand } from './installer-utils'
import {
POSIX_HOOK_JSON_STDIN_PRELUDE,
POSIX_HOOK_JSON_STDIN_READER,
POSIX_HOOK_STDIN_READER,
WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD
} from './hook-stdin-contract'
@@ -561,10 +563,22 @@ describe.skipIf(process.platform === 'win32')('managed hook stdin lifecycle', ()
it('captures stdin before every possible whole-script success exit', async () => {
const scripts = await generatePosixScripts()
for (const [agent, script] of scripts) {
const captureIndex = script.indexOf(`payload=$(${POSIX_HOOK_STDIN_READER})`)
const captureIndex = Math.max(
script.indexOf(`payload=$(${POSIX_HOOK_STDIN_READER})`),
script.indexOf(`payload=$(${POSIX_HOOK_JSON_STDIN_READER})`)
)
const firstExitIndex = script.indexOf('exit 0')
expect(captureIndex, `${agent} payload capture`).toBeGreaterThanOrEqual(0)
expect(firstExitIndex, `${agent} first success exit`).toBeGreaterThan(captureIndex)
// Why: the JSON reader dereferences a variable the prelude sets, so a script
// that carries the reader must carry its prelude above the capture line.
if (script.includes(POSIX_HOOK_JSON_STDIN_READER)) {
const prelude = POSIX_HOOK_JSON_STDIN_PRELUDE.join('\n')
expect(script.indexOf(prelude), `${agent} JSON reader prelude`).toBeGreaterThanOrEqual(0)
expect(script.indexOf(prelude), `${agent} prelude before capture`).toBeLessThan(
captureIndex
)
}
}
})
@@ -0,0 +1,262 @@
// Why an executable suite rather than shape assertions: the reader is a Python
// program embedded in a shell string, so only running it catches a decode that
// raises on a chunk boundary — the shape looked correct while a CJK payload
// crashed the interpreter and fell through to the `cat` hang it exists to avoid.
import { execFile, spawn } from 'node:child_process'
import { promisify } from 'node:util'
import { describe, expect, it } from 'vitest'
import {
buildPosixHookPayloadCapture,
POSIX_HOOK_JSON_STDIN,
POSIX_HOOK_JSON_STDIN_FIRST_BYTE_TIMEOUT_SECONDS,
POSIX_HOOK_JSON_STDIN_PRELUDE,
POSIX_HOOK_JSON_STDIN_IDLE_TIMEOUT_SECONDS
} from './hook-stdin-contract'
const execFileAsync = promisify(execFile)
/** The reader plus a line that prints what it captured, so the payload is observable. */
const READER_SCRIPT = [
...buildPosixHookPayloadCapture('empty-object', POSIX_HOOK_JSON_STDIN).slice(0, -3),
'printf %s "$payload"'
].join('\n')
const REPLACEMENT_CHARACTER = ''
const KILL_AFTER_MS = 9_000
type ReaderRun = {
readonly exitCode: number | null
readonly stdout: string
readonly stderr: string
readonly durationMs: number
readonly timedOut: boolean
}
/** Feeds `chunks` with `gapMs` between them. `closeStdin: false` is the Grok
* SessionStart shape: the payload is written and the pipe is left open. */
function runReader(
chunks: readonly Buffer[],
{
gapMs = 5,
closeStdin = false,
env
}: { gapMs?: number; closeStdin?: boolean; env?: NodeJS.ProcessEnv } = {}
): Promise<ReaderRun> {
return new Promise((resolve, reject) => {
const startedAt = Date.now()
const child = spawn('/bin/sh', ['-c', READER_SCRIPT], {
stdio: ['pipe', 'pipe', 'pipe'],
env: env ?? process.env
})
let stdout = Buffer.alloc(0)
let stderr = ''
let timedOut = false
child.stdout.on('data', (chunk: Buffer) => {
stdout = Buffer.concat([stdout, chunk])
})
child.stderr.on('data', (chunk: Buffer) => {
stderr += chunk.toString()
})
// A reader that never returns leaves the writer's pipe unread; ignore the tear-down error.
child.stdin.on('error', () => {})
const timer = setTimeout(() => {
timedOut = true
child.kill('SIGKILL')
}, KILL_AFTER_MS)
child.on('error', (error) => {
clearTimeout(timer)
reject(error)
})
child.on('close', (exitCode) => {
clearTimeout(timer)
resolve({
exitCode,
stdout: stdout.toString('utf8'),
stderr,
durationMs: Date.now() - startedAt,
timedOut
})
})
void (async () => {
for (const chunk of chunks) {
child.stdin.write(chunk)
await new Promise((resolveGap) => setTimeout(resolveGap, gapMs))
}
if (closeStdin) {
child.stdin.end()
}
})()
})
}
async function resolveDefaultPathPython(): Promise<boolean> {
try {
const { stdout } = await execFileAsync('/bin/sh', [
'-c',
'command -pv python3 || command -pv python'
])
return stdout.trim().length > 0
} catch {
return false
}
}
const hasPython = process.platform === 'win32' ? false : await resolveDefaultPathPython()
describe('POSIX hook JSON stdin reader shape', () => {
// Why: the Python program is carried in a single-quoted shell assignment, so one
// apostrophe would end the string and splice the rest of it into the hook as code.
it('carries no single quote that would escape its shell quoting', () => {
const prelude = POSIX_HOOK_JSON_STDIN_PRELUDE.join('\n')
expect(prelude.split("'")).toHaveLength(3)
})
it('guards HOME before the interpreter runs', () => {
expect(POSIX_HOOK_JSON_STDIN.reader.indexOf('unset HOME')).toBeLessThan(
POSIX_HOOK_JSON_STDIN.reader.indexOf('python3')
)
})
})
describe.skipIf(process.platform === 'win32')('POSIX hook JSON stdin reader', () => {
// Why skipped rather than failed: without an interpreter the chain falls back to
// `cat`, whose read-to-EOF genuinely cannot return while the writer holds the pipe.
const itWithPython = it.skipIf(!hasPython)
itWithPython(
'keeps a multi-byte character intact when it is split across reads',
async () => {
const payload = '{"hook_event_name":"session_start","cwd":"/tmp/漢字","tool":"🚀"}'
// One byte per write: every multi-byte sequence therefore straddles a read.
const chunks = [...Buffer.from(`${payload}\n`, 'utf8')].map((byte) => Buffer.from([byte]))
const result = await runReader(chunks)
expect(result.timedOut, 'reader returned').toBe(false)
expect(result.exitCode).toBe(0)
expect(result.stdout).not.toContain(REPLACEMENT_CHARACTER)
expect(result.stdout).toBe(payload)
},
KILL_AFTER_MS + 1_000
)
// Why: the chain is `python3 || python || cat`, and a reader that died after
// consuming bytes would hand the next one a truncated stream. A non-zero exit
// must therefore imply nothing was read.
itWithPython(
'never hands a partially consumed stream to the fallback reader',
async () => {
const payload = `{"hook_event_name":"session_start","pad":"${'p'.repeat(200_000)}","cwd":"/漢"}`
const bytes = Buffer.from(`${payload}\n`, 'utf8')
// Split inside the 3-byte sequence, with a gap long enough that the first
// read has already completed before the continuation bytes are written.
const splitAt = bytes.length - 4
const chunks = [bytes.subarray(0, splitAt), bytes.subarray(splitAt)]
const result = await runReader(chunks, { gapMs: 400, closeStdin: true })
expect(result.timedOut, 'reader returned').toBe(false)
expect(result.exitCode).toBe(0)
const parsePayload = (): unknown => JSON.parse(result.stdout)
expect(parsePayload).not.toThrow()
expect(result.stdout).toBe(payload)
},
KILL_AFTER_MS + 1_000
)
// Why: re-serialising the object would rewrite non-ASCII as \uXXXX and reorder
// keys, so the hook server would no longer see what the agent actually sent.
itWithPython(
'emits the payload text unchanged rather than re-serialising it',
async () => {
const payload = '{"z":"日本語","a":1,"nested":{"b":[1,2]}}'
const result = await runReader([Buffer.from(`${payload}\n`, 'utf8')])
expect(result.timedOut, 'reader returned').toBe(false)
expect(result.stdout).not.toContain('\\u')
expect(result.stdout).toBe(payload)
},
KILL_AFTER_MS + 1_000
)
// Why: raw_decode does not skip leading whitespace, so a padded payload would
// otherwise never complete and would sit out the idle timeout before returning.
itWithPython(
'returns immediately for a payload preceded by whitespace',
async () => {
const payload = '{"hook_event_name":"session_start"}'
const result = await runReader([Buffer.from(`\n ${payload}\n`, 'utf8')])
expect(result.timedOut, 'reader returned').toBe(false)
expect(result.stdout).toBe(payload)
expect(result.durationMs).toBeLessThan(POSIX_HOOK_JSON_STDIN_IDLE_TIMEOUT_SECONDS * 1_000)
},
KILL_AFTER_MS + 1_000
)
// Why: the first-byte wait is not the idle wait. A host that is slow to schedule
// the writer must not have its payload silently dropped.
itWithPython(
'waits past the idle timeout for a writer that has not sent its first byte',
async () => {
const payload = '{"hook_event_name":"session_start"}'
const result = await runReader([Buffer.alloc(0), Buffer.from(`${payload}\n`)], {
gapMs: 2_500
})
expect(result.timedOut, 'reader returned').toBe(false)
expect(result.stdout).toBe(payload)
expect(result.durationMs).toBeLessThan(
POSIX_HOOK_JSON_STDIN_FIRST_BYTE_TIMEOUT_SECONDS * 1_000
)
},
KILL_AFTER_MS + 1_000
)
// Why: not every hook payload is JSON, and the reader replaces `cat` for Grok —
// it still has to hand back everything a closed stream contained.
itWithPython(
'reads a non-JSON payload through to EOF',
async () => {
const result = await runReader([Buffer.from('not json at all\nsecond line\n')], {
closeStdin: true
})
expect(result.timedOut, 'reader returned').toBe(false)
expect(result.stdout).toBe('not json at all\nsecond line')
},
KILL_AFTER_MS + 1_000
)
// Why: macOS resolves /usr/bin/python3 through an Xcode stub that re-runs its
// whole tool lookup when it cannot reach a cache under $HOME. A HOME pointing
// nowhere cost ~6.6s per spawn, which on its own overran Grok's 10s budget.
itWithPython(
'stays fast when HOME points at a directory that does not exist',
async () => {
const payload = '{"hook_event_name":"session_start"}'
const result = await runReader([Buffer.from(`${payload}\n`, 'utf8')], {
env: { ...process.env, HOME: '/nonexistent/orca-hook-home' }
})
expect(result.timedOut, 'reader returned').toBe(false)
expect(result.stdout).toBe(payload)
expect(result.durationMs).toBeLessThan(2_000)
},
KILL_AFTER_MS + 1_000
)
itWithPython(
'reports nothing on stderr on any of these paths',
async () => {
const result = await runReader([Buffer.from('{"a":"漢"}\n', 'utf8')])
expect(result.stderr).toBe('')
},
KILL_AFTER_MS + 1_000
)
})
+3 -2
View File
@@ -5,7 +5,8 @@ import {
} from '../agent-hooks/installer-utils'
import {
buildPosixHookPayloadCapture,
buildPosixHookSpoolLines
buildPosixHookSpoolLines,
POSIX_HOOK_JSON_STDIN
} from '../agent-hooks/hook-stdin-contract'
import {
buildWindowsGrokHookScript,
@@ -35,7 +36,7 @@ export function getGrokManagedScript(target: 'local' | 'posix' = 'local'): strin
return [
'#!/bin/sh',
...buildPosixHookPayloadCapture(),
...buildPosixHookPayloadCapture('exit', POSIX_HOOK_JSON_STDIN),
...buildPosixHookSpoolLines('grok'),
'if [ -n "$ORCA_AGENT_HOOK_ENDPOINT" ] && [ -r "$ORCA_AGENT_HOOK_ENDPOINT" ]; then',
' . "$ORCA_AGENT_HOOK_ENDPOINT" 2>/dev/null || :',
@@ -0,0 +1,92 @@
import { spawn } from 'node:child_process'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { getGrokManagedScript } from './grok-hook-script'
// Grok's own hook timeout; the budget these cases have to stay well inside.
const GROK_HOOK_TIMEOUT_MS = 10_000
const SESSION_START_BUDGET_MS = 1_500
describe.skipIf(process.platform === 'win32')('Grok POSIX hook stdin without EOF', () => {
let dir = ''
afterEach(() => {
if (dir) {
rmSync(dir, { recursive: true, force: true })
}
})
/** Writes the payload and leaves the pipe open, which is what Grok SessionStart does. */
async function runHookWithoutEof(
chunks: readonly Buffer[]
): Promise<{ exitCode: number | null; durationMs: number; stderr: string }> {
dir = mkdtempSync(join(tmpdir(), 'orca-grok-hook-no-eof-'))
const scriptPath = join(dir, 'grok-hook.sh')
writeFileSync(scriptPath, getGrokManagedScript('posix'), { mode: 0o755 })
const startedAt = Date.now()
const child = spawn('/bin/sh', [scriptPath], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
ORCA_PANE_KEY: 'pane-1',
ORCA_AGENT_HOOK_PORT: '',
ORCA_AGENT_HOOK_TOKEN: '',
ORCA_AGENT_HOOK_ENDPOINT: ''
}
})
let stderr = ''
child.stderr.on('data', (chunk: Buffer) => {
stderr += chunk.toString()
})
child.stdin.on('error', () => {})
const exitCode = await new Promise<number | null>((resolve, reject) => {
const timeout = setTimeout(() => {
child.kill('SIGKILL')
reject(new Error(`hook still blocked on stdin after ${GROK_HOOK_TIMEOUT_MS}ms`))
}, GROK_HOOK_TIMEOUT_MS)
child.on('error', (error) => {
clearTimeout(timeout)
reject(error)
})
child.on('close', (code) => {
clearTimeout(timeout)
resolve(code)
})
void (async () => {
for (const chunk of chunks) {
child.stdin.write(chunk)
await new Promise((resolveGap) => setTimeout(resolveGap, 5))
}
})()
})
return { exitCode, durationMs: Date.now() - startedAt, stderr }
}
it('returns after one JSON object when the caller never closes stdin (SessionStart)', async () => {
const result = await runHookWithoutEof([
Buffer.from('{"hook_event_name":"session_start","session_id":"abc"}\n')
])
expect(result.exitCode).toBe(0)
expect(result.durationMs).toBeLessThan(SESSION_START_BUDGET_MS)
})
// Why: a non-ASCII payload arriving in pieces used to crash the reader, which
// fell through to `cat` and reinstated the very 10s timeout this hook avoids.
it('returns just as fast when a multi-byte payload is split across writes', async () => {
const bytes = Buffer.from(
'{"hook_event_name":"session_start","cwd":"/tmp/漢字","tool":"🚀"}\n',
'utf8'
)
const result = await runHookWithoutEof([...bytes].map((byte) => Buffer.from([byte])))
expect(result.stderr).toBe('')
expect(result.exitCode).toBe(0)
expect(result.durationMs).toBeLessThan(SESSION_START_BUDGET_MS)
}, 20_000)
})
+13 -2
View File
@@ -29,7 +29,10 @@ vi.mock('os', async () => {
import { getGrokToolEventMatcherForTests, GrokHookService } from './hook-service'
import { buildWindowsGrokHookScript } from './windows-grok-hook-script'
import { POSIX_HOOK_STDIN_READER } from '../agent-hooks/hook-stdin-contract'
import {
POSIX_HOOK_JSON_STDIN_PRELUDE,
POSIX_HOOK_JSON_STDIN_READER
} from '../agent-hooks/hook-stdin-contract'
const GROK_SCRIPT_FILE_NAME = process.platform === 'win32' ? 'grok-hook.cmd' : 'grok-hook.sh'
const WINDOWS_POWERSHELL_LAUNCHER =
@@ -298,7 +301,15 @@ describe('GrokHookService', () => {
} else {
// Why: payload is piped to curl via stdin (`payload@-`) so it never lands
// on the curl command line (EDR oversized-command-line false positive).
expect(script).toContain(`payload=$(${POSIX_HOOK_STDIN_READER})`)
// Why the ordering: the reader chain dereferences the prelude's variable, so a
// prelude emitted after the capture would silently run `python -c ""` and
// hand back an empty payload.
const prelude = POSIX_HOOK_JSON_STDIN_PRELUDE.join('\n')
expect(script).toContain(prelude)
expect(script.indexOf(prelude)).toBeLessThan(
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('--data-urlencode "payload@-"')
expect(script).toContain('${#GROK_HOME}" -le 4096')