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
View>Output>Hooks
This shows stderr with the real reason for exit 1.- Cursor Settings > Hooks > Execution Log
Handy, but it’s limited to 100 entries. - Full log without trimming
Command Palette>Developer: Open Logs Folder, then checkcursor.hooks.logfor the current session.
Most common exit 1 causes on Windows
- 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, thenJSON.parse(stdin)throwsSyntaxError. 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 doprocess.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. - Accessing a missing payload field
If the code does something likepayload.text.includes(...)orpayload.message...without a guard, you’ll get aTypeErrorand exit 1. workspace_rootson Windows
Paths can come in a weird format like"/d:/code/code/"with a leading slash and a lowercase drive letter. A directfs.existsSync(payload.workspace_roots[0])can fail unless you normalize.- 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 likerequire('./helpers')orfs.readFileSync('./config.json')can break. Use__dirnamefor files next to the script and a normalizedworkspace_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.jsonfiles (project, user, ProgramData if present) - The full section from
View>Output>Hooksfrom the moment you start the agent - Your
works-cursor-task-track-start.js - Cursor version
Help>Aboutandnode -v(Node 18 vs 20+ can behave differently withprocess.stdin)