New: watch your coding agents work, step by step, on a live board. Try the demo, no account

Cursor hooks: every event, the hooks.json format and working examples

By Jake Marsh, who builds Taskhold. Updated 2026-11-01. Facts about other tools checked 2026-11-01. 6 min read.

Short answer: Cursor hooks are scripts Cursor runs at points in the agent loop, such as before a shell command, after a file edit or when the agent stops. You list them in hooks.json (~/.cursor/ or .cursor/ in a project). Each gets JSON on stdin and can answer with JSON, for example permission deny or a followup_message. Exit code 2 blocks the action.

Hooks let you run your own code around Cursor's agent: block a risky command, format files after every edit, log what the agent ran, or keep it working until the tests pass. This page lists every event, the file format, what each hook can return, and a few examples you can paste. To build a hooks.json from tested recipes, use the Cursor hooks.json generator.

Everything here comes from Cursor's hooks documentation, checked on the date below. For the same idea in other agents, see Claude Code hooks and Gemini CLI hooks. To watch several agents at once, try the live board demo.

Where hooks.json lives

  • User: ~/.cursor/hooks.json. Relative commands run from ~/.cursor/.
  • Project: .cursor/hooks.json in the repo, committed with the code. Runs only in a trusted workspace; relative commands run from the project root.
  • Team: set in the Cursor dashboard on Enterprise plans and synced to members.
  • Enterprise: /Library/Application Support/Cursor/hooks.json on macOS, /etc/cursor/hooks.json on Linux, C:\ProgramData\Cursor\hooks.json on Windows.

Every matching hook from every level runs. When their answers differ, deny beats ask and ask beats allow, whichever file they came from. user_message and agent_message are joined together.

The file format

{
  "version": 1,
  "hooks": {
    "beforeShellExecution": [
      { "command": "./hooks/guard-shell.sh", "matcher": "rm -rf|git push --force", "timeout": 10 }
    ],
    "afterFileEdit": [
      { "command": "./hooks/format.sh" }
    ]
  }
}
version is required and is 1. Each event takes an array of hooks.
  • command: a shell command, an absolute path or a path relative to the hooks.json location.
  • matcher: a regex. For beforeShellExecution it is tested against the full command; for preToolUse against the tool name (Shell, Read, Write, Grep, Delete, Task or MCP:<tool>). Empty or * matches everything.
  • timeout: seconds.
  • failClosed: true makes a crash, timeout or empty output block the action. By default a broken hook lets the action through.
  • loop_limit: for stop and subagentStop, how many automatic follow-ups are allowed (default 5, null for no limit).
  • type: "prompt" runs a model prompt instead of a command (not in cloud agents).

Every agent event

Every agent event. Tab completions have their own two hooks (beforeTabFileRead, afterTabFileEdit), and workspaceOpen runs when a workspace opens.
EventWhen it runsWhat it can return
sessionStartA new conversation startsenv variables, additional_context
beforeSubmitPromptYou hit send, before the request goes outcontinue: false to block, user_message
preToolUseBefore any tool (Shell, Read, Write, MCP, Task...)permission allow or deny, updated_input, messages
beforeShellExecutionBefore a shell commandpermission allow, deny or ask, messages
beforeMCPExecutionBefore an MCP tool callpermission allow, deny or ask, messages
beforeReadFileBefore the agent reads a filepermission allow or deny
postToolUseAfter a tool succeedsadditional_context, replaced MCP output
postToolUseFailureAfter a tool fails, times out or is deniedadditional_context
afterShellExecutionAfter a shell command, with its outputnothing (for logging)
afterMCPExecutionAfter an MCP tool call, with its resultnothing (for logging)
afterFileEditAfter the agent edits a filenothing (run a formatter here)
subagentStart / subagentStopAround a Task subagentpermission on start; followup_message on stop
preCompactBefore context compactionuser_message (it can't block)
afterAgentResponse / afterAgentThoughtAfter a reply or a thinking blocknothing
stopThe agent loop ends (completed, aborted or error)followup_message, sent as your next message
sessionEndThe conversation endsnothing

Tab completions have their own two hooks (beforeTabFileRead, afterTabFileEdit), and workspaceOpen runs when a workspace opens.

Input and exit codes

Each hook gets one JSON object on stdin. Every event includes conversation_id, generation_id, hook_event_name, cursor_version, workspace_roots, user_email and transcript_path, plus its own fields: command and cwd for beforeShellExecution, file_path and edits for afterFileEdit, status and loop_count for stop.

  • Exit 0: success. Cursor reads the JSON you print to stdout.
  • Exit 2: block the action, the same as answering permission deny. Claude Code hooks use the same convention.
  • Any other exit code: the hook failed and the action goes ahead, unless failClosed is true.
  • Permission hooks (preToolUse, beforeShellExecution, beforeMCPExecution, beforeReadFile, subagentStart) block the action if they print invalid JSON, even without failClosed.

Example: block force pushes

#!/bin/bash
# ~/.cursor/hooks/guard-shell.sh (beforeShellExecution)
cmd=$(jq -r '.command')
if echo "$cmd" | grep -qE 'git push (-f|--force)'; then
  echo '{"permission":"deny","user_message":"Force push blocked by a hook.","agent_message":"Force pushes are not allowed. Push normally or ask me."}'
  exit 0
fi
echo '{"permission":"allow"}'
Needs jq. Make it executable with chmod +x.

Example: keep going until the tests pass

#!/bin/bash
# .cursor/hooks/tests-pass.sh (stop)
status=$(jq -r '.status')
[ "$status" = "completed" ] || { echo '{}'; exit 0; }
if ! bun test >/dev/null 2>&1; then
  echo '{"followup_message":"The tests are failing. Run bun test, fix the failures, then stop."}'
else
  echo '{}'
fi
loop_limit (default 5) caps how many times the stop hook can send the agent back.

Limits

  • Cloud agents run command hooks only, skip the user-level ~/.cursor/hooks.json, and don't run sessionStart, sessionEnd, the MCP execution hooks, the Tab hooks or workspaceOpen.
  • Project hooks need a trusted workspace.
  • On Windows the desktop app runs hook commands through PowerShell.
  • preToolUse accepts ask in its schema but doesn't enforce it; use beforeShellExecution or beforeMCPExecution for ask.

Hooks and a shared task board

Hooks see one agent at a time. When several agents run at once, Taskhold puts all of their tasks on one live board: Cursor reports steps and progress through the Taskhold MCP tools, and asks you questions there instead of stopping in the chat. A stop hook covers the runs that end without a word; the ready-made version is on Cursor hook for Taskhold, and connecting Cursor takes one click on the Cursor setup page.

Share this page:XLinkedInBlueskyHacker News

See how far along your coding agents are.

$12/month or $120/year after the trial. Card up front. Cancel before day 7 and you pay nothing. Every agent you run is included.

Questions

Where do I put hooks.json for Cursor?
~/.cursor/hooks.json applies to every project for you. .cursor/hooks.json in a repo applies to that project and can be committed so the team shares it. Project hooks run only in a trusted workspace.
How do I block a command with a Cursor hook?
Use beforeShellExecution with a matcher for the command, and print {"permission":"deny"} or exit with code 2. Add user_message and agent_message to explain why to you and to the agent.
Can a Cursor hook make the agent keep working?
Yes. A stop hook can return followup_message, which Cursor sends as your next message. loop_limit, 5 by default, caps how many times that can happen in a row.
Are Cursor hooks the same as Claude Code hooks?
They are close but not the same. Both take JSON on stdin and treat exit code 2 as block, but the event names and output fields differ, so a Claude Code hook needs changes before Cursor can run it.

Sources

Related

Updated 2026-11-01. Something wrong or out of date? Email hello@greatwork.company.