[Go to site: main page, start]

Skip to main content

Command Palette

Search for a command to run...

Customize

Hooks

Hooks let you observe, control, and extend the agent loop using custom scripts. Define hooks in hooks.json files at the project or user level, or install them through plugins from Customize. Hooks are spawned processes that communicate over stdio using JSON in both directions. They run before or after defined stages of the agent loop and can observe, block, or modify behavior.

With hooks, you can:

  • Run formatters after edits
  • Add analytics for events
  • Scan for PII or secrets
  • Gate risky operations (e.g., SQL writes)
  • Control subagent (Task tool) execution
  • Inject context at session start

Hook categories

Hooks fall into three categories based on what triggers them:

Agent hooks (Cmd+K/Agent Chat) fire during an agent session:

  • sessionStart / sessionEnd - Session lifecycle management
  • preToolUse / postToolUse / postToolUseFailure - Generic tool use hooks (fires for all tools)
  • subagentStart / subagentStop - Subagent (Task tool) lifecycle
  • beforeShellExecution / afterShellExecution - Control shell commands
  • beforeMCPExecution / afterMCPExecution - Control MCP tool usage
  • beforeReadFile / afterFileEdit - Control file access and edits
  • beforeSubmitPrompt - Validate prompts before submission
  • preCompact - Observe context window compaction
  • stop - Handle agent completion
  • afterAgentResponse / afterAgentThought - Track agent responses

Tab hooks (inline completions) fire for autonomous Tab operations:

  • beforeTabFileRead - Control file access for Tab completions
  • afterTabFileEdit - Post-process Tab edits

App lifecycle hooks fire outside any agent session:

  • workspaceOpen - Fires when Cursor opens a workspace and on every workspace folder change. Can return additional plugin paths to load for the current workspace.

These separate hook surfaces let you apply different policies to autonomous Tab operations, user-directed Agent operations, and workspace startup.

Cloud agent support

Cloud agents run command-based hooks from your repository. If you have hooks defined in .cursor/hooks.json at the root of your project, cloud agents pick them up and run them during their work.

On Enterprise plans, cloud agents also run team hooks and enterprise-managed hooks configured through the web dashboard.

Cloud agents sometimes begin in a read-only environment for early exploratory turns. Hooks do not run during those turns. They start once the agent has a writable environment.

Supported hooks

The following hooks run in cloud agents:

HookSupported
beforeShellExecutionYes
afterShellExecutionYes
beforeReadFileYes
afterFileEditYes
preToolUseYes
postToolUseYes
postToolUseFailureYes
subagentStartYes
subagentStopYes
beforeSubmitPromptYes
preCompactYes
afterAgentResponseYes
afterAgentThoughtYes
stopYes

Hooks not available in cloud agents

Some hooks don't apply to cloud agents due to differences in the execution environment:

HookReason
sessionStartDeferred while cloud agents can still start in a read-only environment. Hooks don't load there, so a cloud sessionStart would fire too late (after the first write) rather than at true session start.
sessionEndCloud agents have no editor-lifetime session boundary. sessionEnd is tied to the IDE session, not a cloud agent chat.
beforeMCPExecution / afterMCPExecutionDeferred while cloud agents can still start in a read-only environment, where hooks don't load and MCP hook timing is unclear.
beforeTabFileRead / afterTabFileEditTab completions are an IDE feature and don't run in cloud agents.
workspaceOpenThis is an IDE lifecycle hook and doesn't apply to cloud agents.

Configuration sources

Cloud agents load hooks from these sources:

  • Project hooks (.cursor/hooks.json in your repo): Loaded and run during cloud agent work.
  • Team hooks (Enterprise): Distributed from the dashboard and run in cloud agents.
  • Enterprise hooks (Enterprise): System-wide managed hooks run in cloud agents.

User-level hooks (~/.cursor/hooks.json) are not available in cloud agents. Cloud agent VMs don't have access to your local home directory configuration.

Execution type limits

Cloud agents run command-based hooks only. Prompt-based hooks require authentication wiring between the hook and the agent loop, which isn't available in the cloud execution environment.

Quickstart

Create a hooks.json file. You can create it at the project level (<project>/.cursor/hooks.json) or in your home directory (~/.cursor/hooks.json). Project-level hooks apply only to that specific project, while home directory hooks apply globally.

For user-level hooks that apply globally, create ~/.cursor/hooks.json:

{  "version": 1,  "hooks": {    "afterFileEdit": [{ "command": "./hooks/format.sh" }]  }}

Create your hook script at ~/.cursor/hooks/format.sh:

#!/bin/bash# Read input, do something, exit 0cat > /dev/nullexit 0

Make it executable:

chmod +x ~/.cursor/hooks/format.sh

Cursor watches hooks config files and reloads them automatically. Your hook runs after every file edit.

Hook Types

Hooks support two execution types: command-based (default) and prompt-based (LLM-evaluated).

Command-Based Hooks

Command hooks execute shell scripts that receive JSON input via stdin and return JSON output via stdout.

{  "hooks": {    "beforeShellExecution": [      {        "command": "./scripts/approve-network.sh",        "timeout": 30,        "matcher": "curl|wget|nc"      }    ]  }}

Exit code behavior:

  • Exit code 0 - Hook succeeded, use the JSON output
  • Exit code 2 - Block the action (equivalent to returning permission: "deny")
  • Other exit codes - Hook failed, action proceeds (fail-open by default)

Prompt-Based Hooks

Prompt hooks use an LLM to evaluate a natural language condition. They're useful for policy enforcement without writing custom scripts.

{  "hooks": {    "beforeShellExecution": [      {        "type": "prompt",        "prompt": "Does this command look safe to execute? Only allow read-only operations.",        "timeout": 10      }    ]  }}

Features:

  • Returns structured { ok: boolean, reason?: string } response
  • Uses a fast model for quick evaluation
  • $ARGUMENTS placeholder is auto-replaced with hook input JSON
  • If $ARGUMENTS is absent, hook input is auto-appended
  • Optional model field to override the default LLM model

Examples

{  "version": 1,  "hooks": {    "sessionStart": [      {        "command": "./hooks/session-init.sh"      }    ],    "sessionEnd": [      {        "command": "./hooks/audit.sh"      }    ],    "beforeShellExecution": [      {        "command": "./hooks/audit.sh"      },      {        "command": "./hooks/block-git.sh"      }    ],    "beforeMCPExecution": [      {        "command": "./hooks/audit.sh"      }    ],    "afterShellExecution": [      {        "command": "./hooks/audit.sh"      }    ],    "afterMCPExecution": [      {        "command": "./hooks/audit.sh"      }    ],    "afterFileEdit": [      {        "command": "./hooks/audit.sh"      }    ],    "beforeSubmitPrompt": [      {        "command": "./hooks/audit.sh"      }    ],    "preCompact": [      {        "command": "./hooks/audit.sh"      }    ],    "stop": [      {        "command": "./hooks/audit.sh"      }    ],    "beforeTabFileRead": [      {        "command": "./hooks/redact-secrets-tab.sh"      }    ],    "afterTabFileEdit": [      {        "command": "./hooks/format-tab.sh"      }    ]  }}

TypeScript stop automation hook

Choose TypeScript when you need typed JSON, durable file I/O, and HTTP calls in the same hook. This Bun-powered stop hook tracks per-conversation failure counts on disk, forwards structured telemetry to an internal API, and can automatically schedule a retry when the agent fails twice in a row.

{  "version": 1,  "hooks": {    "stop": [      {        "command": "bun run .cursor/hooks/track-stop.ts --stop"      }    ]  }}

Set AGENT_TELEMETRY_URL to the internal endpoint that should receive run summaries.

Python manifest guard hook

Python shines when you need rich parsing libraries. This hook uses pyyaml to inspect Kubernetes manifests before kubectl apply runs; Bash would struggle to parse multi-document YAML safely.

{  "version": 1,  "hooks": {    "beforeShellExecution": [      {        "command": "python3 .cursor/hooks/kube_guard.py"      }    ]  }}

Install PyYAML (for example, pip install pyyaml) wherever your hook scripts run so the parser import succeeds.

Partner Integrations

We partner with ecosystem vendors who have built hooks support with Cursor. These integrations cover security scanning, governance, secrets management, and more.

MCP governance and visibility

PartnerDescription
MintMCPBuild a complete inventory of MCP servers, monitor tool usage patterns, and scan responses for sensitive data before it reaches the AI model.
Oasis SecurityEnforce least-privilege policies on AI agent actions and maintain full audit trails across enterprise systems.
RunlayerWrap MCP tools and integrate with their MCP broker for centralized control and visibility over agent-to-tool interactions.

Code security and best practices

PartnerDescription
CorridorGet real-time feedback on code implementation and security design decisions as code is being written.
SemgrepAutomatically scan AI-generated code for vulnerabilities with real-time feedback to regenerate code until security issues are resolved.

Dependency security

PartnerDescription
Endor LabsIntercept package installations and scan for malicious dependencies, preventing supply chain attacks before they enter your codebase.

Agent security and safety

PartnerDescription
SnykReview agent actions in real-time with Evo Agent Guard, detecting and preventing issues like prompt injection and dangerous tool calls.

Secrets management

PartnerDescription
1PasswordValidate that environment files from 1Password Environments are properly mounted before shell commands execute, enabling just-in-time secrets access without writing credentials to disk.

For more details about our hooks partners, see the Hooks for security and platform teams blog post.

Configuration

Define hooks in a hooks.json file. Configuration can exist at multiple levels. All matching hooks from every source run; when responses conflict, higher-priority sources take precedence during merge:

~/.cursor/├── hooks.json└── hooks/    ├── audit.sh    └── block-git.sh
  • Enterprise (MDM-managed, system-wide):
    • macOS: /Library/Application Support/Cursor/hooks.json
    • Linux/WSL: /etc/cursor/hooks.json
    • Windows: C:\\ProgramData\\Cursor\\hooks.json
  • Team (Cloud-distributed, enterprise only):
    • Configured in the web dashboard and synced to all team members automatically
  • Project (Project-specific):
    • <project-root>/.cursor/hooks.json
    • Project hooks run in any trusted workspace and are checked into version control with your project
  • User (User-specific):
    • ~/.cursor/hooks.json

Priority order (highest to lowest): Enterprise → Team → Project → User

The hooks object maps hook names to arrays of hook definitions. Each definition currently supports a command property that can be a shell string, an absolute path, or a relative path. The working directory depends on the hook source:

  • Project hooks (.cursor/hooks.json in a repository): Run from the project root
  • User hooks (~/.cursor/hooks.json): Run from ~/.cursor/
  • Enterprise hooks (system-wide config): Run from the enterprise config directory
  • Team hooks (cloud-distributed): Run from the managed hooks directory

For project hooks, use paths like .cursor/hooks/script.sh (relative to project root), not ./hooks/script.sh (which would look for <project>/hooks/script.sh).

Configuration file

This example shows a user-level hooks file (~/.cursor/hooks.json). For project-level hooks, change paths like ./hooks/script.sh to .cursor/hooks/script.sh:

{  "version": 1,  "hooks": {    "sessionStart": [{ "command": "./session-init.sh" }],    "sessionEnd": [{ "command": "./audit.sh" }],    "preToolUse": [      {        "command": "./hooks/validate-tool.sh",        "matcher": "Shell|Read|Write"      }    ],    "postToolUse": [{ "command": "./hooks/audit-tool.sh" }],    "subagentStart": [{ "command": "./hooks/validate-subagent.sh" }],    "subagentStop": [{ "command": "./hooks/audit-subagent.sh" }],    "beforeShellExecution": [{ "command": "./script.sh" }],    "afterShellExecution": [{ "command": "./script.sh" }],    "afterMCPExecution": [{ "command": "./script.sh" }],    "afterFileEdit": [{ "command": "./format.sh" }],    "preCompact": [{ "command": "./audit.sh" }],    "stop": [{ "command": "./audit.sh", "loop_limit": 10 }],    "beforeTabFileRead": [{ "command": "./redact-secrets-tab.sh" }],    "afterTabFileEdit": [{ "command": "./format-tab.sh" }],    "workspaceOpen": [{ "command": "./register-workspace-plugins.sh" }]  }}

The Agent hooks (sessionStart, sessionEnd, preToolUse, postToolUse, postToolUseFailure, subagentStart, subagentStop, beforeShellExecution, afterShellExecution, beforeMCPExecution, afterMCPExecution, beforeReadFile, afterFileEdit, beforeSubmitPrompt, preCompact, stop, afterAgentResponse, afterAgentThought) apply to Cmd+K and Agent Chat operations. The Tab hooks (beforeTabFileRead, afterTabFileEdit) apply specifically to inline Tab completions. The app lifecycle hook (workspaceOpen) fires when a workspace opens and on workspace folder changes, independent of any agent session.

Global Configuration Options

OptionTypeDefaultDescription
versionnumber1Config schema version

Per-Script Configuration Options

OptionTypeDefaultDescription
commandstringrequiredScript path or command
type"command" | "prompt""command"Hook execution type
timeoutnumberplatform defaultExecution timeout in seconds
loop_limitnumber | null5Per-script loop limit for stop/subagentStop hooks. null means no limit. Default is 5 for Cursor hooks, null for Claude Code hooks.
failClosedbooleanfalseWhen true, hook failures (crash, timeout, invalid JSON) block the action instead of allowing it through. Useful for security-critical hooks.
matcherobject-Filter criteria for when hook runs

Matcher Configuration

Matchers let you filter when a hook runs. Which field the matcher applies to depends on the hook:

{  "hooks": {    "preToolUse": [      {        "command": "./validate-shell.sh",        "matcher": "Shell"      }    ],    "subagentStart": [      {        "command": "./validate-explore.sh",        "matcher": "explore|shell"      }    ],    "beforeShellExecution": [      {        "command": "./approve-network.sh",        "matcher": "curl|wget|nc "      }    ]  }}
  • subagentStart: The matcher runs against the subagent type (e.g. explore, shell, generalPurpose). Use it to run hooks only when a specific kind of subagent is started. The example above runs validate-explore.sh only for explore or shell subagents.
  • beforeShellExecution: The matcher runs against the shell command string. Use it to run hooks only when the command matches a pattern (e.g. network calls, file deletions). The example above runs approve-network.sh only when the command contains curl, wget, or nc .

Available matchers by hook:

  • preToolUse / postToolUse / postToolUseFailure: Filter by tool type. Values include Shell, Read, Write, Grep, Delete, Task, and MCP tools using the MCP:<tool_name> format.
  • subagentStart / subagentStop: Filter by subagent type (generalPurpose, explore, shell, etc.).
  • beforeShellExecution / afterShellExecution: Filter by the shell command text; the matcher is matched against the full command string.
  • beforeReadFile: Filter by tool type (TabRead, Read, etc.).
  • afterFileEdit: Filter by tool type (TabWrite, Write, etc.).
  • beforeSubmitPrompt: Matched against the value UserPromptSubmit.
  • stop: Matched against the value Stop.
  • afterAgentResponse: Matched against the value AgentResponse.
  • afterAgentThought: Matched against the value AgentThought.

Team Distribution

Hooks can be distributed to team members using project hooks (via version control), MDM tools, or Cursor's cloud distribution system.

Project Hooks (Version Control)

Project hooks are the simplest way to share hooks with your team. Place a hooks.json file at <project-root>/.cursor/hooks.json and commit it to your repository. When team members open the project in a trusted workspace, Cursor automatically loads and runs the project hooks.

Cloud agents also load these project hooks when they work on your repository in the cloud.

Project hooks:

  • Are stored in version control alongside your code
  • Automatically load for all team members in trusted workspaces
  • Can be project-specific (e.g., enforce formatting standards for a particular codebase)
  • Require the workspace to be trusted to run (for security)

MDM Distribution

Distribute hooks across your organization using Mobile Device Management (MDM) tools. Place the hooks.json file and hook scripts in the target directories on each machine.

User home directory (per-user distribution):

  • ~/.cursor/hooks.json
  • ~/.cursor/hooks/ (for hook scripts)

Global directories (system-wide distribution):

  • macOS: /Library/Application Support/Cursor/hooks.json
  • Linux/WSL: /etc/cursor/hooks.json
  • Windows: C:\\ProgramData\\Cursor\\hooks.json

Note: MDM-based distribution is fully managed by your organization. Cursor does not deploy or manage files through your MDM solution. Ensure your internal IT or security team handles configuration, deployment, and updates in accordance with your organization's policies.

Cloud Distribution (Enterprise Only)

Enterprise teams can use Cursor's native cloud distribution to automatically sync hooks to all team members. Configure hooks in the web dashboard. Cursor automatically delivers configured hooks to all client machines when team members log in.

Cloud distribution provides:

  • Automatic synchronization to all team members (every thirty minutes)
  • Operating system targeting for platform-specific hooks
  • Centralized management through the dashboard

Enterprise administrators can create, edit, and manage team hooks from the dashboard without requiring access to individual machines.

Contact sales to get Enterprise cloud hook distribution.

Reference

Common schema

Input (all hooks)

All hooks receive a base set of fields in addition to their hook-specific fields:

{  "conversation_id": "string",  "generation_id": "string",  "model": "string",  "model_id": "string",  "model_params": [{ "id": "string", "value": "string" }],  "hook_event_name": "string",  "cursor_version": "string",  "workspace_roots": ["<path>"],  "user_email": "string | null",  "transcript_path": "string | null"}
FieldTypeDescription
conversation_idstringStable ID of the conversation across many turns
generation_idstringThe current generation that changes with every user message
modelstringLegacy model slug configured for the composer that triggered the hook
model_idstring (optional)Structured ID for the selected model, when available
model_paramsarray (optional)Selected model parameters, such as thinking, context, or effort. Each item has an id and value.
hook_event_namestringWhich hook is being run
cursor_versionstringCursor application version (e.g. "1.7.2")
workspace_rootsstring[]The list of root folders in the workspace (normally just one, but multiroot workspaces can have multiple)
user_emailstring | nullEmail address of the authenticated user, if available
transcript_pathstring | nullPath to the main conversation transcript file (null if transcripts disabled)

Hook events

preToolUse

Called before any tool execution. This is a generic hook that fires for all tool types (Shell, Read, Write, MCP, Task, etc.). Use matchers to filter by specific tools.

// Input{  "tool_name": "Shell",  "tool_input": { "command": "npm install", "working_directory": "/project" },  "tool_use_id": "abc123",  "cwd": "/project",  "model": "claude-opus-4-7-thinking-max",  "model_id": "claude-opus-4-7",  "model_params": [    { "id": "thinking", "value": "true" },    { "id": "context", "value": "1m" },    { "id": "effort", "value": "max" }  ],  "agent_message": "Installing dependencies..."}// Output{  "permission": "allow" | "deny",  "user_message": "<message shown in client when denied>",  "agent_message": "<message sent to agent when denied>",  "updated_input": { "command": "npm ci" }}
Output FieldTypeDescription
permissionstring"allow" to proceed, "deny" to block. "ask" is accepted by the schema but not enforced for preToolUse today.
user_messagestring (optional)Message shown to the user when the action is denied
agent_messagestring (optional)Message fed back to the agent when the action is denied
updated_inputobject (optional)Modified tool input to use instead

postToolUse

Called after successful tool execution. Useful for auditing, analytics, and injecting context.

// Input{  "tool_name": "Shell",  "tool_input": { "command": "npm test" },  "tool_output": "{\"exitCode\":0,\"stdout\":\"All tests passed\"}",  "tool_use_id": "abc123",  "cwd": "/project",  "duration": 5432,  "model": "claude-opus-4-7-thinking-max",  "model_id": "claude-opus-4-7",  "model_params": [    { "id": "thinking", "value": "true" },    { "id": "context", "value": "1m" },    { "id": "effort", "value": "max" }  ]}// Output{  "updated_mcp_tool_output": { "modified": "output" },  "additional_context": "Test coverage report attached."}
Input FieldTypeDescription
durationnumberExecution time in milliseconds
tool_outputstringJSON-stringified result payload from the tool (not raw terminal text)
Output FieldTypeDescription
updated_mcp_tool_outputobject (optional)For MCP tools only: replaces the tool output seen by the model
additional_contextstring (optional)Extra context injected into the conversation after the tool result

postToolUseFailure

Called when a tool fails, times out, or is denied. Useful for error tracking and recovery logic.

// Input{  "tool_name": "Shell",  "tool_input": { "command": "npm test" },  "tool_use_id": "abc123",  "cwd": "/project",  "error_message": "Command timed out after 30s",  "failure_type": "timeout" | "error" | "permission_denied",  "duration": 5000,  "is_interrupt": false}// Output{  // No output fields currently supported}
Input FieldTypeDescription
error_messagestringDescription of the failure
failure_typestringType of failure: "error", "timeout", or "permission_denied"
durationnumberTime in milliseconds until the failure occurred
is_interruptbooleanWhether this failure was caused by a user interrupt/cancellation

subagentStart

Called before spawning a subagent (Task tool). Can allow or deny subagent creation.

// Input{  "subagent_id": "abc-123",  "subagent_type": "generalPurpose",  "task": "Explore the authentication flow",  "parent_conversation_id": "conv-456",  "tool_call_id": "tc-789",  "subagent_model": "claude-sonnet-4-20250514",  "is_parallel_worker": false,  "git_branch": "feature/auth"}// Output{  "permission": "allow" | "deny",  "user_message": "<message shown when denied>"}
Input FieldTypeDescription
subagent_idstringUnique identifier for this subagent instance
subagent_typestringType of subagent: generalPurpose, explore, shell, etc.
taskstringThe task description given to the subagent
parent_conversation_idstringConversation ID of the parent agent session
tool_call_idstringID of the tool call that triggered the subagent
subagent_modelstringModel the subagent will use
is_parallel_workerbooleanWhether this subagent is running as a parallel worker
git_branchstring (optional)Git branch the subagent will operate on, if applicable
Output FieldTypeDescription
permissionstring"allow" to proceed, "deny" to block. "ask" is not supported for subagentStart and is treated as "deny".
user_messagestring (optional)Message shown to the user when the subagent is denied

subagentStop

Called when a subagent completes, errors, or is aborted. Can trigger follow-up actions.

// Input{  "subagent_type": "generalPurpose",  "status": "completed" | "error" | "aborted",  "task": "Explore the authentication flow",  "description": "Exploring auth flow",  "summary": "<subagent output summary>",  "duration_ms": 45000,  "message_count": 12,  "tool_call_count": 8,  "loop_count": 0,  "modified_files": ["src/auth.ts"],  "agent_transcript_path": "/path/to/subagent/transcript.txt"}// Output{  "followup_message": "<auto-continue with this message>"}
Input FieldTypeDescription
subagent_typestringType of subagent: generalPurpose, explore, shell, etc.
statusstring"completed", "error", or "aborted"
taskstringThe task description given to the subagent
descriptionstringShort description of the subagent's purpose
summarystringOutput summary from the subagent
duration_msnumberExecution time in milliseconds
message_countnumberNumber of messages exchanged during the subagent session
tool_call_countnumberNumber of tool calls the subagent made
loop_countnumberNumber of times a subagentStop follow-up has already triggered for this subagent (starts at 0)
modified_filesstring[]Files the subagent modified
agent_transcript_pathstring | nullPath to the subagent's own transcript file (separate from the parent conversation)
Output FieldTypeDescription
followup_messagestring (optional)Auto-continue with this message. Only consumed when status is "completed".

The followup_message field enables loop-style flows where subagent completion triggers the next iteration. Follow-ups are subject to the same configurable loop limit as the stop hook (default 5, configurable via loop_limit).

beforeShellExecution / beforeMCPExecution

Called before any shell command or MCP tool is executed. Return a permission decision.

// beforeShellExecution input{  "command": "<full terminal command>",  "cwd": "<current working directory>",  "sandbox": false}// beforeMCPExecution input{  "tool_name": "<tool name>",  "tool_input": "<json params>"}// Plus either:{ "url": "<server url>" }// Or:{ "command": "<command string>" }// Output{  "permission": "allow" | "deny" | "ask",  "user_message": "<message shown in client>",  "agent_message": "<message sent to agent>"}

afterShellExecution

Fires after a shell command executes; useful for auditing or collecting metrics from command output.

// Input{  "command": "<full terminal command>",  "output": "<full terminal output>",  "duration": 1234,  "sandbox": false}
FieldTypeDescription
commandstringThe full terminal command that was executed
outputstringFull output captured from the terminal
durationnumberDuration in milliseconds spent executing the shell command (excludes approval wait time)
sandboxbooleanWhether the command ran in a sandboxed environment

afterMCPExecution

Fires after an MCP tool executes; includes the tool's input parameters and full JSON result.

// Input{  "tool_name": "<tool name>",  "tool_input": "<json params>",  "result_json": "<tool result json>",  "duration": 1234}
FieldTypeDescription
tool_namestringName of the MCP tool that was executed
tool_inputstringJSON params string passed to the tool
result_jsonstringJSON string of the tool response
durationnumberDuration in milliseconds spent executing the MCP tool (excludes approval wait time)

afterFileEdit

Fires after the Agent edits a file; useful for formatters or accounting of agent-written code.

// Input{  "file_path": "<absolute path>",  "edits": [{ "old_string": "<search>", "new_string": "<replace>" }]}

beforeReadFile

Called before Agent reads a file. Use for access control to block sensitive files from being sent to the model.

// Input{  "file_path": "<absolute path>",  "content": "<file contents>",  "attachments": [    {      "type": "file" | "rule",      "file_path": "<absolute path>"    }  ]}// Output{  "permission": "allow" | "deny",  "user_message": "<message shown when denied>"}
Input FieldTypeDescription
file_pathstringAbsolute path to the file being read
contentstringFull contents of the file
attachmentsarrayContext attachments associated with the prompt. Each entry has a type ("file" or "rule") and a file_path.
Output FieldTypeDescription
permissionstring"allow" to proceed, "deny" to block
user_messagestring (optional)Message shown to user when denied

beforeTabFileRead

Called before Tab (inline completions) reads a file. Enable redaction or access control before Tab accesses file contents.

Key differences from beforeReadFile:

  • Only triggered by Tab, not Agent
  • Does not include attachments field (Tab doesn't use prompt attachments)
  • Useful for applying different policies to autonomous Tab operations
// Input{  "file_path": "<absolute path>",  "content": "<file contents>"}// Output{  "permission": "allow" | "deny"}

afterTabFileEdit

Called after Tab (inline completions) edits a file. Useful for formatters or auditing of Tab-written code.

Key differences from afterFileEdit:

  • Only triggered by Tab, not Agent
  • Includes detailed edit information: range, old_line, and new_line for precise edit tracking
  • Useful for fine-grained formatting or analysis of Tab edits
// Input{  "file_path": "<absolute path>",  "edits": [    {      "old_string": "<search>",      "new_string": "<replace>",      "range": {        "start_line_number": 10,        "start_column": 5,        "end_line_number": 10,        "end_column": 20      },      "old_line": "<line before edit>",      "new_line": "<line after edit>"    }  ]}// Output{  // No output fields currently supported}

beforeSubmitPrompt

Called right after user hits send but before backend request. Can prevent submission.

// Input{  "prompt": "<user prompt text>",  "attachments": [    {      "type": "file" | "rule",      "file_path": "<absolute path>"    }  ]}// Output{  "continue": true | false,  "user_message": "<message shown to user when blocked>"}
Output FieldTypeDescription
continuebooleanWhether to allow the prompt submission to proceed
user_messagestring (optional)Message shown to the user when the prompt is blocked

afterAgentResponse

Called after the agent has completed an assistant message.

// Input{  "text": "<assistant final text>"}

afterAgentThought

Called after the agent completes a thinking block. Useful for observing the agent's reasoning process.

// Input{  "text": "<fully aggregated thinking text>",  "duration_ms": 5000}// Output{  // No output fields currently supported}
FieldTypeDescription
textstringFully aggregated thinking text for the completed block
duration_msnumber (optional)Duration in milliseconds for the thinking block

stop

Called when the agent loop ends. Can optionally auto-submit a follow-up user message to keep iterating.

// Input{  "status": "completed" | "aborted" | "error",  "loop_count": 0}
// Output{  "followup_message": "<message text>"}
  • The optional followup_message is a string. When provided and non-empty, Cursor will automatically submit it as the next user message. This enables loop-style flows (e.g., iterate until a goal is met).
  • The loop_count field indicates how many times the stop hook has already triggered an automatic follow-up for this conversation (starts at 0). The default limit is 5 auto follow-ups per script, configurable via the loop_limit option. Set loop_limit to null to remove the cap. The same limit applies to subagentStop follow-ups.

sessionStart

Called when a new composer conversation is created. This hook runs as fire-and-forget; the agent loop does not wait for or enforce a blocking response. Use it to set up session-specific environment variables or inject additional context.

// Input{  "session_id": "<unique session identifier>",  "is_background_agent": true | false,  "composer_mode": "agent" | "ask" | "edit"}
// Output{  "env": { "<key>": "<value>" },  "additional_context": "<context to add to conversation>"}
Input FieldTypeDescription
session_idstringUnique identifier for this session (same as conversation_id)
is_background_agentbooleanWhether this is a background agent session vs interactive session
composer_modestring (optional)The mode the composer is starting in (e.g., "agent", "ask", "edit")
Output FieldTypeDescription
envobject (optional)Environment variables to set for this session. Available to all subsequent hook executions
additional_contextstring (optional)Additional context to add to the conversation's initial system context

sessionEnd

Called when a composer conversation ends. This is a fire-and-forget hook useful for logging, analytics, or cleanup tasks. The response is logged but not used.

// Input{  "session_id": "<unique session identifier>",  "reason": "completed" | "aborted" | "error" | "window_close" | "user_close",  "duration_ms": 45000,  "is_background_agent": true | false,  "final_status": "<status string>",  "error_message": "<error details if reason is 'error'>"}
// Output{  // No output fields - fire and forget}
Input FieldTypeDescription
session_idstringUnique identifier for the session that is ending
reasonstringHow the session ended: "completed", "aborted", "error", "window_close", or "user_close"
duration_msnumberTotal duration of the session in milliseconds
is_background_agentbooleanWhether this was a background agent session
final_statusstringFinal status of the session
error_messagestring (optional)Error message if reason is "error"

preCompact

Called before context window compaction/summarization occurs. This is an observational hook that cannot block or modify the compaction behavior. Useful for logging when compaction happens or notifying users.

// Input{  "trigger": "auto" | "manual",  "context_usage_percent": 85,  "context_tokens": 120000,  "context_window_size": 128000,  "message_count": 45,  "messages_to_compact": 30,  "is_first_compaction": true | false}
// Output{  "user_message": "<message to show when compaction occurs>"}
Input FieldTypeDescription
triggerstringWhat triggered the compaction: "auto" or "manual"
context_usage_percentnumberCurrent context window usage as a percentage (0-100)
context_tokensnumberCurrent context window token count
context_window_sizenumberMaximum context window size in tokens
message_countnumberNumber of messages in the conversation
messages_to_compactnumberNumber of messages that will be summarized
is_first_compactionbooleanWhether this is the first compaction for this conversation
Output FieldTypeDescription
user_messagestring (optional)Message to show to the user when compaction occurs

workspaceOpen

Fires once when Cursor opens a workspace and again on every workspace folder change. Skipped when the window has zero workspace folders. Runs in the Cursor desktop app and CLI.

// Input{  "hook_event_name": "workspaceOpen",  "cursor_version": "string",  "workspace_roots": ["<absolute path>"],  "user_email": "string | null"}// Output{  "pluginPaths": ["<absolute path>", "..."]}
Output FieldTypeDescription
pluginPathsstring[] (optional)Absolute paths to plugin directories to load for the current workspace.

Environment Variables

Hook scripts receive environment variables when executed:

VariableDescriptionAlways Present
CURSOR_PROJECT_DIRWorkspace root directoryYes
CURSOR_VERSIONCursor version stringYes
CURSOR_USER_EMAILAuthenticated user emailIf logged in
CURSOR_TRANSCRIPT_PATHPath to the conversation transcript fileIf transcripts enabled
CURSOR_CODE_REMOTESet to the string "true" when running in a remote workspaceFor remote workspaces
CLAUDE_PROJECT_DIRAlias for project dir (Claude compatibility)Yes

Session-scoped environment variables from sessionStart hooks are passed to all subsequent hook executions within that session.

Troubleshooting

How to confirm hooks are active

There is a Hooks tab in Customize and a Hooks output channel to debug configured and executed hooks and see errors.

If hooks are not working

  • Cursor watches hooks.json files and reloads them on save. If hooks still do not load, restart Cursor.
  • Check that relative paths are correct for your hook source:
    • For project hooks, paths are relative to the project root (e.g., .cursor/hooks/script.sh)
    • For user hooks, paths are relative to ~/.cursor/ (e.g., ./hooks/script.sh or hooks/script.sh)

Exit code blocking

Exit code 2 from command hooks blocks the action (equivalent to returning permission: "deny"). This matches Claude Code behavior for compatibility.

Enterprise hooks and distribution

Cloud distribution and team-wide hook management are available on Enterprise.

Contact Sales