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
Looking for ready-to-use integrations? See Partner Integrations for security, governance, and secrets management solutions from our ecosystem partners.
Cursor supports loading hooks from third-party tools like Claude Code. See Third Party Hooks for details on compatibility and configuration.
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 managementpreToolUse/postToolUse/postToolUseFailure- Generic tool use hooks (fires for all tools)subagentStart/subagentStop- Subagent (Task tool) lifecyclebeforeShellExecution/afterShellExecution- Control shell commandsbeforeMCPExecution/afterMCPExecution- Control MCP tool usagebeforeReadFile/afterFileEdit- Control file access and editsbeforeSubmitPrompt- Validate prompts before submissionpreCompact- Observe context window compactionstop- Handle agent completionafterAgentResponse/afterAgentThought- Track agent responses
Tab hooks (inline completions) fire for autonomous Tab operations:
beforeTabFileRead- Control file access for Tab completionsafterTabFileEdit- 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:
| Hook | Supported |
|---|---|
beforeShellExecution |
Yes |
afterShellExecution |
Yes |
beforeReadFile |
Yes |
afterFileEdit |
Yes |
preToolUse |
Yes |
postToolUse |
Yes |
postToolUseFailure |
Yes |
subagentStart |
Yes |
subagentStop |
Yes |
beforeSubmitPrompt |
Yes |
preCompact |
Yes |
afterAgentResponse |
Yes |
afterAgentThought |
Yes |
stop |
Yes |
Hooks not available in cloud agents¶
Some hooks don’t apply to cloud agents due to differences in the execution environment:
| Hook | Reason |
|---|---|
sessionStart |
Deferred 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. |
sessionEnd |
Cloud agents have no editor-lifetime session boundary. sessionEnd is tied to the IDE session, not a cloud agent chat. |
beforeMCPExecution / afterMCPExecution |
Deferred while cloud agents can still start in a read-only environment, where hooks don’t load and MCP hook timing is unclear. |
beforeTabFileRead / afterTabFileEdit |
Tab completions are an IDE feature and don’t run in cloud agents. |
workspaceOpen |
This 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.jsonin 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.
User hooks (\~/.cursor/)¶
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 0
cat > /dev/null
exit 0
Make it executable:
chmod +x ~/.cursor/hooks/format.sh
Project hooks (.cursor/)¶
For project-level hooks that apply to a specific repository, create <project>/.cursor/hooks.json:
{
"version": 1,
"hooks": {
"afterFileEdit": [{ "command": ".cursor/hooks/format.sh" }]
}
}
Note: Project hooks run from the project root, so use .cursor/hooks/format.sh (not ./hooks/format.sh).
Create your hook script at <project>/.cursor/hooks/format.sh:
#!/bin/bash
# Read input, do something, exit 0
cat > /dev/null
exit 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 returningpermission: "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
$ARGUMENTSplaceholder is auto-replaced with hook input JSON- If
$ARGUMENTSis absent, hook input is auto-appended - Optional
modelfield to override the default LLM model
Examples¶
The examples below use ./hooks/... paths, which work for user hooks (~/.cursor/hooks.json) where scripts run from ~/.cursor/. For project hooks (<project>/.cursor/hooks.json), use .cursor/hooks/... paths instead since scripts run from the project root.
```json title=”hooks.json”
{
“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”
}
]
}
}
```sh title="audit.sh"
#!/bin/bash
# audit.sh - Hook script that writes all JSON input to /tmp/agent-audit.log
# This script is designed to be called by Cursor's hooks system for auditing purposes
# Read JSON input from stdin
json_input=$(cat)
# Create timestamp for the log entry
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# Create the log directory if it doesn't exist
mkdir -p "$(dirname /tmp/agent-audit.log)"
# Write the timestamped JSON entry to the audit log
echo "[$timestamp] $json_input" >> /tmp/agent-audit.log
# Exit successfully
exit 0
```sh title=”block-git.sh”
!/bin/bash¶
Hook to block git commands and redirect to gh tool usage¶
This hook implements the beforeShellExecution hook from the Cursor Hooks Spec¶
Initialize debug logging¶
echo “Hook execution started” >> /tmp/hooks.log
Read JSON input from stdin¶
input=$(cat)
echo “Received input: $input” >> /tmp/hooks.log
Parse the command from the JSON input¶
command=\((echo "\)input” | jq -r ‘.command // empty’)
echo “Parsed command: ‘$command’” >> /tmp/hooks.log
Check if the command contains ‘git’ or ‘gh’¶
if [[ “\(command" =~ git[[:space:]] ]] || [[ "\)command” == “git” ]]; then
echo “Git command detected - blocking: ‘\(command'" >> /tmp/hooks.log
# Block the git command and provide guidance to use gh tool instead
cat << EOF
{
"continue": true,
"permission": "deny",
"user_message": "Git command blocked. Please use the GitHub CLI (gh) tool instead.",
"agent_message": "The git command '\)command’ has been blocked by a hook. Instead of using raw git commands, please use the ‘gh’ tool which provides better integration with GitHub and follows best practices. For example:\n- Instead of ‘git clone’, use ‘gh repo clone’\n- Instead of ‘git push’, use ‘gh repo sync’ or the appropriate gh command\n- For other git operations, check if there’s an equivalent gh command or use the GitHub web interface\n\nThis helps maintain consistency and leverages GitHub’s enhanced tooling.”
}
EOF
elif [[ “\(command" =~ gh[[:space:]] ]] || [[ "\)command” == “gh” ]]; then
echo “GitHub CLI command detected - asking for permission: ‘$command’” >> /tmp/hooks.log
# Ask for permission for gh commands
cat << EOF
{
“continue”: true,
“permission”: “ask”,
“user_message”: “GitHub CLI command requires permission: \(command",
"agent_message": "The command '\)command’ uses the GitHub CLI (gh) which can interact with your GitHub repositories and account. Please review and approve this command if you want to proceed.”
}
EOF
else
echo “Non-git/non-gh command detected - allowing: ‘$command’” >> /tmp/hooks.log
# Allow non-git/non-gh commands
cat << EOF
{
“continue”: true,
“permission”: “allow”
}
EOF
fi
### 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.
```json title="hooks.json"
{
"version": 1,
"hooks": {
"stop": [
{
"command": "bun run .cursor/hooks/track-stop.ts --stop"
}
]
}
}
```ts title=”.cursor/hooks/track-stop.ts”
import { mkdir, readFile, writeFile } from ‘node:fs/promises’;
import { stdin } from ‘bun’;
type StopHookInput = {
conversation_id: string;
generation_id: string;
model: string;
model_id?: string;
model_params?: Array<{ id: string; value: string }>;
status: ‘completed’ | ‘aborted’ | ‘error’;
loop_count: number;
};
type StopHookOutput = {
followup_message?: string;
};
type MetricsEntry = {
lastStatus: StopHookInput[‘status’];
errorCount: number;
lastUpdatedIso: string;
};
type MetricsStore = Record
const STATE_DIR = ‘.cursor/hooks/state’;
const METRICS_PATH = ${STATE_DIR}/agent-metrics.json;
const TELEMETRY_URL = Bun.env.AGENT_TELEMETRY_URL;
async function parseHookInput
const text = await stdin.text();
return JSON.parse(text) as T;
}
async function readMetrics(): Promise
try {
return JSON.parse(await readFile(METRICS_PATH, ‘utf8’)) as MetricsStore;
} catch {
return {};
}
}
async function writeMetrics(store: MetricsStore) {
await mkdir(STATE_DIR, { recursive: true });
await writeFile(METRICS_PATH, JSON.stringify(store, null, 2), ‘utf8’);
}
async function sendTelemetry(payload: StopHookInput, entry: MetricsEntry) {
if (!TELEMETRY_URL) return;
await fetch(TELEMETRY_URL, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({
conversationId: payload.conversation_id,
generationId: payload.generation_id,
model: payload.model,
modelId: payload.model_id,
modelParams: payload.model_params,
status: payload.status,
errorCount: entry.errorCount,
loopCount: payload.loop_count,
timestamp: entry.lastUpdatedIso
})
});
}
async function main() {
const payload = await parseHookInput
const metrics = await readMetrics();
const entry =
metrics[payload.conversation_id] ?? {
lastStatus: payload.status,
errorCount: 0,
lastUpdatedIso: ‘’
};
entry.lastStatus = payload.status;
entry.lastUpdatedIso = new Date().toISOString();
entry.errorCount = payload.status === ‘error’ ? entry.errorCount + 1 : 0;
metrics[payload.conversation_id] = entry;
await writeMetrics(metrics);
await sendTelemetry(payload, entry);
const response: StopHookOutput = {};
if (entry.errorCount >= 2 && payload.loop_count < 4) {
response.followup_message =
‘Automated retry triggered after two failures. Double-check credentials before running again.’;
}
process.stdout.write(JSON.stringify(response) + ‘\n’);
}
main().catch(error => {
console.error(‘[stop hook] failed’, error);
process.stdout.write(‘{}\n’);
});
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.
```json title="hooks.json"
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"command": "python3 .cursor/hooks/kube_guard.py"
}
]
}
}
```python title=”.cursor/hooks/kube_guard.py”
!/usr/bin/env python3¶
import json
import shlex
import sys
from pathlib import Path
import yaml
SENSITIVE_NAMESPACES = {“prod”, “production”}
def main() -> None:
payload = json.load(sys.stdin)
command = payload.get(“command”, “”)
cwd = Path(payload.get(“cwd”) or “.”)
response = {“continue”: True, “permission”: “allow”}
try:
args = shlex.split(command)
except ValueError:
print(json.dumps(response))
return
if len(args) < 2 or args[0] != "kubectl" or args[1] != "apply" or "-f" not in args:
print(json.dumps(response))
return
f_index = args.index("-f")
if f_index + 1 >= len(args):
print(json.dumps(response))
return
manifest_arg = args[f_index + 1]
manifest_path = (cwd / manifest_arg).resolve()
if not manifest_path.exists():
print(json.dumps(response))
return
cli_namespace = None
for i, arg in enumerate(args):
if arg in ("-n", "--namespace") and i + 1 < len(args):
cli_namespace = args[i + 1]
elif arg.startswith("--namespace="):
cli_namespace = arg.split("=", 1)[1]
elif arg.startswith("-n="):
cli_namespace = arg.split("=", 1)[1]
try:
documents = list(yaml.safe_load_all(manifest_path.read_text()))
except (OSError, yaml.YAMLError) as exc:
sys.stderr.write(f"Failed to read/parse {manifest_path}: {exc}\n")
print(json.dumps(response))
return
if cli_namespace in SENSITIVE_NAMESPACES or any(
(doc or {}).get("metadata", {}).get("namespace") in SENSITIVE_NAMESPACES
for doc in documents
):
response.update(
{
"permission": "ask",
"user_message": "kubectl apply to prod requires manual approval.",
"agent_message": f"{manifest_path.name} includes protected namespaces; confirm with your team before continuing.",
}
)
print(json.dumps(response))
if name == “main”:
main()
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
| Partner | Description |
| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| [MintMCP](https://www.mintmcp.com/blog/mcp-governance-cursor-hooks) | Build a complete inventory of MCP servers, monitor tool usage patterns, and scan responses for sensitive data before it reaches the AI model. |
| [Oasis Security](https://www.oasis.security/blog/cursor-oasis-governing-agentic-access) | Enforce least-privilege policies on AI agent actions and maintain full audit trails across enterprise systems. |
| [Runlayer](https://www.runlayer.com/blog/cursor-hooks) | Wrap MCP tools and integrate with their MCP broker for centralized control and visibility over agent-to-tool interactions. |
### Code security and best practices
| Partner | Description |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| [Corridor](https://corridor.dev/blog/corridor-cursor-hooks/) | Get real-time feedback on code implementation and security design decisions as code is being written. |
| [Semgrep](https://semgrep.dev/blog/2025/cursor-hooks-mcp-server) | Automatically scan AI-generated code for vulnerabilities with real-time feedback to regenerate code until security issues are resolved. |
### Dependency security
| Partner | Description |
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| [Endor Labs](https://www.endorlabs.com/learn/bringing-malware-detection-into-ai-coding-workflows-with-cursor-hooks) | Intercept package installations and scan for malicious dependencies, preventing supply chain attacks before they enter your codebase. |
### Agent security and safety
| Partner | Description |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| [Snyk](https://snyk.io/blog/evo-agent-guard-cursor-integration/) | Review agent actions in real-time with Evo Agent Guard, detecting and preventing issues like prompt injection and dangerous tool calls. |
### Secrets management
| Partner | Description |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [1Password](https://marketplace.1password.com/integration/cursor-hooks) | Validate 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](https://cursor.com/blog/hooks-partners) 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:
```sh
~/.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.jsonin 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¶
| Option | Type | Default | Description |
|---|---|---|---|
version |
number | 1 |
Config schema version |
Per-Script Configuration Options¶
| Option | Type | Default | Description |
|---|---|---|---|
command |
string | required | Script path or command |
type |
"command" | "prompt" |
"command" |
Hook execution type |
timeout |
number | platform default | Execution timeout in seconds |
loop_limit |
number | null | 5 |
Per-script loop limit for stop/subagentStop hooks. null means no limit. Default is 5 for Cursor hooks, null for Claude Code hooks. |
failClosed |
boolean | false |
When true, hook failures (crash, timeout, invalid JSON) block the action instead of allowing it through. Useful for security-critical hooks. |
matcher |
object | - | 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 runsvalidate-explore.shonly 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.shonly when the command containscurl,wget, ornc.
Available matchers by hook:
- preToolUse / postToolUse / postToolUseFailure: Filter by tool type. Values include
Shell,Read,Write,Grep,Delete,Task, and MCP tools using theMCP:<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"
}
| Field | Type | Description |
|---|---|---|
conversation_id |
string | Stable ID of the conversation across many turns |
generation_id |
string | The current generation that changes with every user message |
model |
string | Legacy model slug configured for the composer that triggered the hook |
model_id |
string (optional) | Structured ID for the selected model, when available |
model_params |
array (optional) | Selected model parameters, such as thinking, context, or effort. Each item has an id and value. |
hook_event_name |
string | Which hook is being run |
cursor_version |
string | Cursor application version (e.g. “1.7.2”) |
workspace_roots |
string[] | The list of root folders in the workspace (normally just one, but multiroot workspaces can have multiple) |
user_email |
string | null | Email address of the authenticated user, if available |
transcript_path |
string | null | Path to the main conversation transcript file (null if transcripts disabled) |
App lifecycle hooks (workspaceOpen) fire outside any agent session, so the request omits conversation_id, generation_id, model, session_id, and transcript_path. They still receive hook_event_name, cursor_version, workspace_roots, and user_email.
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 Field | Type | Description |
|---|---|---|
permission |
string | "allow" to proceed, "deny" to block. "ask" is accepted by the schema but not enforced for preToolUse today. |
user_message |
string (optional) | Message shown to the user when the action is denied |
agent_message |
string (optional) | Message fed back to the agent when the action is denied |
updated_input |
object (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 Field | Type | Description |
|---|---|---|
duration |
number | Execution time in milliseconds |
tool_output |
string | JSON-stringified result payload from the tool (not raw terminal text) |
| Output Field | Type | Description |
|---|---|---|
updated_mcp_tool_output |
object (optional) | For MCP tools only: replaces the tool output seen by the model |
additional_context |
string (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 Field | Type | Description |
|---|---|---|
error_message |
string | Description of the failure |
failure_type |
string | Type of failure: "error", "timeout", or "permission_denied" |
duration |
number | Time in milliseconds until the failure occurred |
is_interrupt |
boolean | Whether 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 Field | Type | Description |
|---|---|---|
subagent_id |
string | Unique identifier for this subagent instance |
subagent_type |
string | Type of subagent: generalPurpose, explore, shell, etc. |
task |
string | The task description given to the subagent |
parent_conversation_id |
string | Conversation ID of the parent agent session |
tool_call_id |
string | ID of the tool call that triggered the subagent |
subagent_model |
string | Model the subagent will use |
is_parallel_worker |
boolean | Whether this subagent is running as a parallel worker |
git_branch |
string (optional) | Git branch the subagent will operate on, if applicable |
| Output Field | Type | Description |
|---|---|---|
permission |
string | "allow" to proceed, "deny" to block. "ask" is not supported for subagentStart and is treated as "deny". |
user_message |
string (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 Field | Type | Description |
|---|---|---|
subagent_type |
string | Type of subagent: generalPurpose, explore, shell, etc. |
status |
string | "completed", "error", or "aborted" |
task |
string | The task description given to the subagent |
description |
string | Short description of the subagent’s purpose |
summary |
string | Output summary from the subagent |
duration_ms |
number | Execution time in milliseconds |
message_count |
number | Number of messages exchanged during the subagent session |
tool_call_count |
number | Number of tool calls the subagent made |
loop_count |
number | Number of times a subagentStop follow-up has already triggered for this subagent (starts at 0) |
modified_files |
string[] | Files the subagent modified |
agent_transcript_path |
string | null | Path to the subagent’s own transcript file (separate from the parent conversation) |
| Output Field | Type | Description |
|---|---|---|
followup_message |
string (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.
By default, hook failures (crash, timeout, invalid JSON) allow the action through (fail-open). Set failClosed: true on the hook definition to block the action on failure instead. This is recommended for security-critical beforeMCPExecution hooks.
// beforeShellExecution input
{
"command": "<full terminal command>",
"cwd": "<current working directory>",
"sandbox": false
}
// beforeMCPExecution input
{
"tool_name": "<tool name>",
"tool_input": "<json params>",
"mcp_server_name": "<server name from mcp.json>"
}
// Plus either (HTTP/SSE servers):
{ "url": "<server url>", "mcp_server_url": "<server url>" }
// Or (stdio servers):
{ "command": "<launch command and args>" }
// Output
{
"permission": "allow" | "deny" | "ask",
"user_message": "<message shown in client>",
"agent_message": "<message sent to agent>"
}
| Field | Type | Description |
|---|---|---|
tool_name |
string | Name of the MCP tool about to run |
tool_input |
string | JSON params string that will be passed to the tool |
mcp_server_name |
string | The server’s key in its mcp.json (for example, linear). Use this to recognize a specific server. |
mcp_server_url |
string | Server URL, present only for HTTP/SSE servers |
url |
string | Same as mcp_server_url; present only for HTTP/SSE servers |
command |
string | The stdio launch command and arguments joined with spaces; present only for stdio servers |
Match on mcp_server_name (and tool_name) to decide whether a call targets your server. command is the launch string from the server’s config and can differ between installs: relative paths, ${CURSOR_PLUGIN_ROOT} expansion, or an HTTP transport (which has no command at all). A hook that allows anything it does not recognize should treat a missing or unexpected mcp_server_name as a deny.
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
}
| Field | Type | Description |
|---|---|---|
command |
string | The full terminal command that was executed |
output |
string | Full output captured from the terminal |
duration |
number | Duration in milliseconds spent executing the shell command (excludes approval wait time) |
sandbox |
boolean | Whether 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>",
"mcp_server_name": "<server name from mcp.json>",
"result_json": "<tool result json>",
"duration": 1234
}
| Field | Type | Description |
|---|---|---|
tool_name |
string | Name of the MCP tool that was executed |
tool_input |
string | JSON params string passed to the tool |
mcp_server_name |
string | The server’s key in its mcp.json |
mcp_server_url |
string | Server URL, present only for HTTP/SSE servers |
result_json |
string | JSON string of the tool response |
duration |
number | Duration 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.
By default, beforeReadFile hook failures (crash, timeout, invalid JSON) are logged and the read is allowed through. Set failClosed: true on the hook definition to block the read on failure instead.
// 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 Field | Type | Description |
|---|---|---|
file_path |
string | Absolute path to the file being read |
content |
string | Full contents of the file |
attachments |
array | Context attachments associated with the prompt. Each entry has a type ("file" or "rule") and a file_path. |
| Output Field | Type | Description |
|---|---|---|
permission |
string | "allow" to proceed, "deny" to block |
user_message |
string (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
attachmentsfield (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, andnew_linefor 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 Field | Type | Description |
|---|---|---|
continue |
boolean | Whether to allow the prompt submission to proceed |
user_message |
string (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
}
| Field | Type | Description |
|---|---|---|
text |
string | Fully aggregated thinking text for the completed block |
duration_ms |
number (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_messageis 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_countfield 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 theloop_limitoption. Setloop_limittonullto remove the cap. The same limit applies tosubagentStopfollow-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 Field | Type | Description |
|---|---|---|
session_id |
string | Unique identifier for this session (same as conversation_id) |
is_background_agent |
boolean | Whether this is a background agent session vs interactive session |
composer_mode |
string (optional) | The mode the composer is starting in (e.g., “agent”, “ask”, “edit”) |
| Output Field | Type | Description |
|---|---|---|
env |
object (optional) | Environment variables to set for this session. Available to all subsequent hook executions |
additional_context |
string (optional) | Additional context to add to the conversation’s initial system context |
The schema also accepts continue and user_message fields, but current callers do not enforce them. Session creation is not blocked even when continue is false.
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 Field | Type | Description |
|---|---|---|
session_id |
string | Unique identifier for the session that is ending |
reason |
string | How the session ended: “completed”, “aborted”, “error”, “window_close”, or “user_close” |
duration_ms |
number | Total duration of the session in milliseconds |
is_background_agent |
boolean | Whether this was a background agent session |
final_status |
string | Final status of the session |
error_message |
string (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 Field | Type | Description |
|---|---|---|
trigger |
string | What triggered the compaction: “auto” or “manual” |
context_usage_percent |
number | Current context window usage as a percentage (0-100) |
context_tokens |
number | Current context window token count |
context_window_size |
number | Maximum context window size in tokens |
message_count |
number | Number of messages in the conversation |
messages_to_compact |
number | Number of messages that will be summarized |
is_first_compaction |
boolean | Whether this is the first compaction for this conversation |
| Output Field | Type | Description |
|---|---|---|
user_message |
string (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 Field | Type | Description |
|---|---|---|
pluginPaths |
string[] (optional) | Absolute paths to plugin directories to load for the current workspace. |
Environment Variables¶
Hook scripts receive environment variables when executed:
| Variable | Description | Always Present |
|---|---|---|
CURSOR_PROJECT_DIR |
Workspace root directory | Yes |
CURSOR_VERSION |
Cursor version string | Yes |
CURSOR_USER_EMAIL |
Authenticated user email | If logged in |
CURSOR_TRANSCRIPT_PATH |
Path to the conversation transcript file | If transcripts enabled |
CURSOR_CODE_REMOTE |
Set to the string "true" when running in a remote workspace |
For remote workspaces |
CLAUDE_PROJECT_DIR |
Alias 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.jsonfiles 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.shorhooks/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.