[HOOKS] Hook 1 failed with exit code 1 undefined

Hey, let’s break it down. exit code 1 means the script itself crashed. The hooks infrastructure ran fine and just reported the exit code. The undefined at the end is just log noise and doesn’t affect debugging.

Hook names

beforeSubmitPrompt and stop in your config are both valid, active events. Alongside the classic before* and after*, Cursor also added newer names: preToolUse, postToolUse, subagentStart, subagentStop, preCompact, sessionStart, sessionEnd, and workspaceOpen (App lifecycle) Hooks | Cursor Docs. These are additions, not renames. beforeSubmitPrompt is still supported. If you saw UserPromptSubmit or PreToolUse in PascalCase, that’s Claude Code’s API, not Cursor, and it’s easy to mix them up.

Also worth knowing about known issues with beforeSubmitPrompt in recent versions: updated_input gets silently dropped Bug: `beforeSubmitPrompt` hook `updated_input` is silently stripped — modified prompt never reaches the model, and block sometimes doesn’t block beforeSubmitPrompt hook block is broken. This isn’t directly related to your exit 1, but it’s good context. If your script’s goal is to “tell” something to the agent, the most reliable approach right now is using stop plus followup_message.

Where to see the script stderr

  1. View > Output > Hooks
    This shows stderr with the real reason for exit 1.
  2. Cursor Settings > Hooks > Execution Log
    Handy, but it’s limited to 100 entries.
  3. Full log without trimming
    Command Palette > Developer: Open Logs Folder, then check cursor.hooks.log for the current session.

Most common exit 1 causes on Windows

  1. UTF-8 in stdin
    Confirmed Windows bug: the payload can arrive without proper UTF-8. Non ASCII characters turn into ?, multibyte chars can get cut, then JSON.parse(stdin) throws SyntaxError. If your prompts can include Cyrillic or emoji, this is the top suspect. Related threads:
    [Windows] Hooks receive corrupted UTF-8 (? instead of Cyrillic)
    Hook stdin pipe double-encodes non-ASCII on non-UTF-8 Windows
    In Node, explicitly do process.stdin.setEncoding('utf8') before reading. System workaround: Control Panel > Region > Administrative > Beta: Use Unicode UTF-8 for worldwide language support, then restart Cursor. It doesn’t always help.
  2. Accessing a missing payload field
    If the code does something like payload.text.includes(...) or payload.message... without a guard, you’ll get a TypeError and exit 1.
  3. workspace_roots on Windows
    Paths can come in a weird format like "/d:/code/code/" with a leading slash and a lowercase drive letter. A direct fs.existsSync(payload.workspace_roots[0]) can fail unless you normalize.
  4. Unstable hook cwd
    The hook process working directory isn’t always the workspace root and it has changed between releases:
    Inconsistent working directory for plugin hook commands
    Stop hook uses wrong (or different) working directory when executing
    Relative paths like require('./helpers') or fs.readFileSync('./config.json') can break. Use __dirname for files next to the script and a normalized workspace_roots[0] for project paths.

Response contract

JSON in stdin, JSON out to stdout. Not stderr. No BOM. No prefix console.log before the JSON. Response fields are snake_case like user_message, followup_message, not camelCase. By event:

  • beforeSubmitPrompt: {"continue": true|false, "user_message": "..."} or just {}
  • stop: {"followup_message": "..."} optional, or {}

At the end, use process.stdout.write(...) and an explicit process.exit(0) after flush.

On Windows there’s another known issue where valid JSON can show up as {} in the Execution Log: Stop hook followup_message not captured on Windows – Execution Log shows {} despite valid JSON on stdout
It doesn’t cause exit 1, but keep it in mind.

About the PowerShell variant

-File has limited support with stdin piping. The most reliable setup is calling node.exe directly from hooks.json like your first variant. If you still need PowerShell, read stdin explicitly:

$payload = [Console]::In.ReadToEnd() | ConvertFrom-Json

Manual check to rule out Cursor

$payload = '{"hook_event_name":"beforeSubmitPrompt","prompt":"test","conversation_id":"x","generation_id":"y","workspace_roots":["/c:/Users/yi.chen/"]}'
$payload | node C:/Users/yi.chen/.cursor/hooks/works-cursor-task-track-start.js

If it fails, the issue is in the script. If it works cleanly but fails inside Cursor, it’s likely PATH or cwd or payload parsing, see the UTF-8 point above.

Duplicate hooks

Cursor loads three hooks.json levels in parallel:

  • project local: <workspace>/.cursor/hooks.json
  • user global: C:\Users\<user>\.cursor\hooks.json
  • enterprise or system: C:\ProgramData\Cursor\hooks.json

If the same script is registered in multiple places, you’ll see multiple runs for one event. Check for old or duplicated configs.

What to share for an exact diagnosis

  • All three hooks.json files (project, user, ProgramData if present)
  • The full section from View > Output > Hooks from the moment you start the agent
  • Your works-cursor-task-track-start.js
  • Cursor version Help > About and node -v (Node 18 vs 20+ can behave differently with process.stdin)