fix(cursor): accept BOM-prefixed hook JSON (#12652)

* fix(cursor): accept BOM-prefixed hook JSON

* test(cursor): pin the hook BOM allowance to one leading U+FEFF

Document why the BOM strip exists and cover the narrowness the fix
claims: a double BOM, a whitespace-then-BOM prefix, and a BOM inside
the JSON body are all still rejected.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
This commit is contained in:
plaonn 2026-08-10 07:23:55 +09:00 committed by GitHub
parent 8331fe5995
commit e20845eedb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 45 additions and 2 deletions

View File

@ -152,6 +152,45 @@ describe('shared agent-hook-listener', () => {
expect(event!.payload.agentType).toBe('claude')
})
it('normalizes a BOM-prefixed Cursor hook payload to a working state', () => {
const event = normalizeHookPayload(
state,
'cursor',
{
paneKey: PANE_KEY,
payload: '\uFEFF{"hook_event_name":"beforeSubmitPrompt","prompt":"Synthetic Cursor prompt"}'
},
'production'
)
expect(event?.payload).toMatchObject({
agentType: 'cursor',
state: 'working',
prompt: 'Synthetic Cursor prompt'
})
expect(event?.hookEventName).toBe('beforeSubmitPrompt')
})
// Why: pins the allowance to exactly one leading U+FEFF, so nobody widens it into a trim.
it('still rejects a hook payload that is malformed once the BOM is removed', () => {
const bom = '\uFEFF'
const body = '{"hook_event_name":"beforeSubmitPrompt"}'
for (const payload of [
`${bom}${bom}${body}`,
`${bom}not json`,
` ${bom}${body}`,
`{"hook_event_name"${bom}:"beforeSubmitPrompt"}`
]) {
const event = normalizeHookPayload(
state,
'cursor',
{ paneKey: PANE_KEY, payload },
'production'
)
expect(event).toBeNull()
}
})
it('normalizes Gemini BeforeTool to working with tool fields', () => {
const event = normalizeHookPayload(
state,

View File

@ -85,8 +85,12 @@ const AGENT_HOOK_JSON_STRUCTURE_LIMITS = {
} as const
function parseAgentHookJson(content: string): unknown {
assertJsonTextStructureWithinLimits(content, AGENT_HOOK_JSON_STRUCTURE_LIMITS)
return JSON.parse(content) as unknown
// Why: Cursor on Windows writes UTF-8-with-BOM to the hook's stdin and `JSON.parse` rejects U+FEFF,
// so the whole event was dropped. Strip exactly one leading BOM — not a trim — to keep every other
// malformed payload rejected as before.
const normalizedContent = content.charCodeAt(0) === 0xfeff ? content.slice(1) : content
assertJsonTextStructureWithinLimits(normalizedContent, AGENT_HOOK_JSON_STRUCTURE_LIMITS)
return JSON.parse(normalizedContent) as unknown
}
/** Bound the warn-once Sets so a client varying `version`/`env` per request can't grow them unbounded. */