Cursor IDE Hooks: Exhaustive Technical Reference
> Last updated: January 2026 > Covers: Cursor v1.7 through v2.3.x
Overview
Cursor IDE Hooks: Exhaustive Technical Reference
Last updated: January 2026 Covers: Cursor v1.7 through v2.3.x
Table of Contents
- Overview
- Configuration
- Hook Types
- Input/Output Schemas
- Permission System
- Version History and Breaking Changes
- Known Bugs and Regressions
- Platform-Specific Issues
- Comparison with Claude Code and Gemini
- Security Considerations
- Edge Cases and Gotchas
- Debugging
- Enterprise Features
- Sources
Overview
Cursor Hooks were introduced in version 1.7 (October 2025) as a mechanism to observe, control, and extend the agent loop using custom scripts. Hooks are spawned as standalone processes that communicate via JSON over stdio.
Key Characteristics:
- Hooks are deterministic programs (unlike rules/MCP which are interpreted by the LLM)
- Configuration via JSON files
- Receive structured input on stdin
- Return JSON output on stdout
- Currently in beta - APIs may change
Sources:
Configuration
File Locations (Priority Order)
- Enterprise-managed global directories (MDM deployment)
- Project-level:
<project-root>/.cursor/hooks.json(version controlled) - User home:
~/.cursor/hooks.json(personal automation)
Cursor runs all hooks that apply from each location.
Basic Structure
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{ "command": "./hooks/script.sh" }
],
"afterFileEdit": [
{ "command": "bun run hooks/format.ts" }
]
}
}
Path Resolution
- Relative paths resolve from the
hooks.jsonfile's directory, not the project root - Absolute paths are supported
- Scripts must be executable (
chmod +x)
JSON Schema
Community-maintained schema available at:
https://unpkg.com/cursor-hooks@latest/schema/hooks.schema.json
Enable IntelliSense in Cursor settings:
{
"json.schemas": [
{
"fileMatch": [".cursor/hooks.json"],
"url": "https://unpkg.com/cursor-hooks/schema/hooks.schema.json"
}
]
}
Source: cursor-hooks npm package
Hook Types
Agent Hooks (Cmd+K / Agent Chat)
| Hook | Trigger | Can Block? | Can Message Agent? |
|---|---|---|---|
beforeSubmitPrompt | Before prompt sent to LLM | Yes | No (beta limitation) |
beforeShellExecution | Before shell command runs | Yes | Yes |
afterShellExecution | After shell command completes | No | No |
beforeMCPExecution | Before MCP tool invocation | Yes | Yes |
afterMCPExecution | After MCP tool returns | No | No |
beforeReadFile | Before file content sent to LLM | Yes | No |
afterFileEdit | After agent edits a file | No | No |
afterAgentResponse | After agent text response | No | No |
afterAgentThought | After agent thinking/reasoning | No | No |
stop | When agent loop completes | No | Yes (followup) |
Tab Hooks (Inline Completions)
| Hook | Trigger | Can Block? |
|---|---|---|
beforeTabFileRead | Before file read for Tab completions | Yes |
afterTabFileEdit | After Tab applies an edit | No |
Important: Tab hooks fire very frequently (on cursor movement) and have performance implications, especially on Windows.
Input/Output Schemas
Common Input Fields (All Hooks)
{
"conversation_id": "uuid",
"generation_id": "uuid",
"model": "claude-4-sonnet",
"hook_event_name": "beforeShellExecution",
"cursor_version": "2.1.46",
"workspace_roots": ["/path/to/project"],
"user_email": "user@example.com"
}
beforeShellExecution
Input:
{
"command": "git status",
"cwd": "/path/to/project"
}
Note: The cwd field may be empty string in some cases - this is a known edge case.
Output:
{
"permission": "allow",
"user_message": "Displayed in UI",
"agent_message": "Sent to agent context"
}
afterShellExecution
Input:
{
"command": "git status",
"output": "On branch main...",
"duration": 1234
}
Note: duration excludes user approval wait time.
beforeMCPExecution
Input:
{
"tool_name": "github__create_issue",
"tool_input": "{\\"title\\": \\"...\\"}",
"url": "https://mcp-server.example.com",
"command": "npx @mcp/server"
}
Either url or command is present depending on server configuration.
Output: Same as beforeShellExecution.
afterMCPExecution
Input:
{
"tool_name": "github__create_issue",
"tool_input": "{...}",
"result_json": "{...}",
"duration": 1234
}
beforeReadFile
Input:
{
"file_path": "/absolute/path/to/file.ts",
"content": "file contents..."
}
Output:
{
"permission": "allow"
}
Important: beforeReadFile does not support user_message or agent_message.
afterFileEdit
Input:
{
"file_path": "/absolute/path/to/file.ts",
"edits": [
{
"old_string": "before",
"new_string": "after",
"range": {
"start_line_number": 10,
"start_column": 5,
"end_line_number": 10,
"end_column": 20
},
"old_line": "full line before",
"new_line": "full line after"
}
]
}
Note: This is fire-and-forget - you cannot block or communicate with agent.
beforeSubmitPrompt
Input:
{
"prompt": "user's prompt text",
"attachments": [
{
"type": "file",
"filePath": "/path/to/attachment.ts"
},
{
"type": "rule",
"filePath": "/path/to/.cursorrules"
}
]
}
Output:
{
"continue": true,
"user_message": "Shown if blocked"
}
Beta limitation: Output JSON is currently not fully respected.
afterAgentResponse / afterAgentThought
Input:
{
"text": "agent response or thinking text",
"duration_ms": 5000
}
stop
Input:
{
"status": "completed",
"loop_count": 5
}
Status values: "completed", "aborted", "error"
Output:
{
"followup_message": "Auto-submitted prompt"
}
Limit: Maximum 5 automatic follow-ups to prevent infinite loops.
Permission System
Permission Values
| Value | Behavior |
|---|---|
allow | Execute without user intervention |
deny | Block execution, show user_message |
ask | Prompt user for confirmation |
Response Fields
| Field | Description | Supported Hooks |
|---|---|---|
permission | allow/deny/ask | beforeShellExecution, beforeMCPExecution |
user_message | Displayed in Cursor UI | beforeShellExecution, beforeMCPExecution |
agent_message | Injected into agent context | beforeShellExecution, beforeMCPExecution |
continue | Boolean to proceed | beforeSubmitPrompt |
followup_message | Auto-submitted message | stop |
Version History and Breaking Changes
v1.7 (October 2025)
- Initial release of hooks system
- Introduced: beforeShellExecution, beforeMCPExecution, beforeReadFile, afterFileEdit, stop
- Response fields:
userMessage,agentMessage(camelCase)
v2.0.x (November 2025)
- Added: beforeSubmitPrompt, afterAgentResponse, afterAgentThought
- Added: Tab hooks (beforeTabFileRead, afterTabFileEdit)
- Breaking change: Field names changed to snake_case (
user_message,agent_message) - Sandbox mode introduced
v2.0.64
- Regression:
user_messageandagent_messagecompletely non-functional - Workaround: Downgrade to v1.7
v2.1.x (December 2025)
- v2.1.6:
agent_messagestill broken;followup_messageworks - v2.1.25: Windows hooks stopped working
- v2.1.46: Windows hooks still broken
v2.3.x (January 2026)
- Windows Git Bash / PowerShell injection bug persists
- CLI still has partial hook support (only shell hooks)
Sources:
Known Bugs and Regressions
Critical: Field Name Case Sensitivity
Issue: Documentation specifies snake_case but v1.7 used camelCase.
Solution: Always use snake_case in v2.0+:
{
"permission": "deny",
"user_message": "correct",
"agent_message": "correct"
}
Not:
{
"userMessage": "wrong in v2.0+",
"agentMessage": "wrong in v2.0+"
}
Multiple Hooks Bug
Issue: When multiple hooks are defined in the same trigger array, only the first executes.
{
"hooks": {
"beforeShellExecution": [
{ "command": "./hook1.sh" },
{ "command": "./hook2.sh" }
]
}
}
Only hook1.sh runs.
Status: Acknowledged December 2025, closed December 30, 2025 without fix confirmed.
Source: Forum: Multiple Hooks Bug
CLI vs IDE Hook Support
Issue: Cursor CLI (cursor-agent) only supports a subset of hooks.
| Environment | Supported Hooks |
|---|---|
| IDE | All hooks |
| CLI | beforeShellExecution, afterShellExecution only |
Source: Forum: CLI doesn't send all events
agent_message Not Reaching Context
Issue: In v2.0.64+, the agent_message field is not injected into agent context.
Status: Known regression, no timeline for fix.
Platform-Specific Issues
Windows: General Hook Failures
Symptoms:
"--: line 1: 3: Bad file descriptor"error- Empty output in Hooks panel
- Hooks work in terminal but fail in Cursor
Affected versions: v2.0.38, v2.1.25+, v2.1.46
Workaround: Use PowerShell scripts instead of bash:
{
"hooks": {
"beforeShellExecution": [
{ "command": "powershell.exe -File ./hooks/check.ps1" }
]
}
}
Source: Forum: Cursor Hooks On Windows
Windows: Git Bash / PowerShell Injection
Issue: When Git Bash is set as default terminal, Cursor injects PowerShell syntax that Bash cannot parse.
Error:
"--: eval: line 1: syntax error near unexpected token '[Convert]::FromBase64String'"
Root cause: Cursor uses [Convert]::FromBase64String (PowerShell) to decode hook metadata.
Workaround: Set PowerShell as default terminal for Cursor:
{
"terminal.integrated.defaultProfile.windows": "PowerShell"
}
Source: Forum: Project-level hooks fail on Windows with Git Bash
Windows: beforeTabFileRead Performance
Issue: beforeTabFileRead is extremely slow on Windows (1+ second vs 0.5 second on macOS), causing event queue buildup.
Workaround: Disable beforeTabFileRead if not essential:
{
"version": 1,
"hooks": {
"beforeShellExecution": [...],
"afterFileEdit": [...]
}
}
Source: Forum: beforeTabFileRead hook is extremely slow on Windows
Comparison with Claude Code and Gemini
Claude Code Hooks
| Aspect | Cursor | Claude Code |
|---|---|---|
| Config Location | .cursor/hooks.json | .claude/settings.json |
| Hook Types | 11+ lifecycle events | PreToolUse, PostToolUse, PermissionRequest, SessionEnd |
| Tool Matchers | N/A (hooks apply globally) | Per-tool matching (e.g., "matcher": "Bash") |
| Input Method | stdin JSON | $CLAUDE_TOOL_INPUT env var |
| Exit Codes | Not documented | Exit 2 = deny with message |
| Schema | Community-maintained | Official: json.schemastore.org/claude-code-settings.json |
Claude Code Example:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "if [[ \\"$CLAUDE_TOOL_INPUT\\" == *\\"rm -rf\\"* ]]; then exit 2; fi",
"timeout": 180
}]
}]
}
}
Key Differences:
- Claude Code has granular tool matching; Cursor hooks apply to all tool invocations of a type
- Claude Code uses environment variables; Cursor uses stdin
- Claude Code has official JSON schema; Cursor relies on community
- Claude Code supports regex matchers (
"Edit|MultiEdit|Write"); Cursor does not
Sources:
Gemini Code Assist
As of January 2026, Gemini Code Assist does not have a documented hooks system comparable to Cursor or Claude Code.
Security Considerations
Workspace Trust
Important: Cursor disables VS Code's Workspace Trust by default.
{
"security.workspace.trust.enabled": tru