Cursor Adapter
Native event names: camelCase. Reference doc: https://cursor.com/docs/hooks.
Cursor splits "before tool" into specific events (beforeShellExecution, beforeMCPExecution, beforeReadFile) and uses different response shapes for each event category.
Stdin → unified
Cursor hook_event_name | Unified |
|---|---|
sessionStart | SessionStart |
beforeShellExecution | PreToolUse + tool: "Bash" |
afterShellExecution | PostToolUse + tool: "Bash" |
beforeMCPExecution | PreToolUse + tool: "MCP" |
afterMCPExecution | PostToolUse + tool: "MCP" |
beforeReadFile | PreToolUse + tool: "Read" |
afterFileEdit | PostToolUse + tool: "Edit" |
beforeSubmitPrompt | UserPromptSubmit (preserves composer_mode, attachments in _native) |
preToolUse | PreToolUse (tool from tool_name) |
postToolUse | PostToolUse |
stop | Stop |
Other Cursor events (afterAgentResponse, afterTabFileEdit, preCompact, subagentStart / subagentStop, sessionEnd) are not supported; the adapter throws on them.
Response categories
Cursor uses five response shapes, one per event category:
Permission events: preToolUse, beforeShellExecution, beforeMCPExecution, beforeReadFile
{
"permission": "allow" | "deny" | "ask",
"user_message": "shown to the human",
"agent_message": "shown to the model",
"updated_input": { /* preToolUse only */ }
}Mapping:
decision→permissionreason→agent_messageuser_message→user_messagemodified_input→updated_input(only onpreToolUse; dropped + warn on the specific events)
Prompt submit: beforeSubmitPrompt
{
"continue": true | false,
"user_message": "shown when blocked"
}decision: "deny"→continue: falsedecision: "allow"or undefined →continue: truedecision: "ask"→continue: false(Cursor's prompt-submit doesn't support ask)user_message→user_messagereason→ dropped + warn (no model-facing channel here)additional_context→ dropped + warn (Cursor's prompt-submit can't inject context)
Context injection: sessionStart, postToolUse
{
"additional_context": "injected to model context"
}additional_context→additional_contextdecision,reason,user_message,modified_input→ dropped + warn
Stop
{
"followup_message": "continuation prompt for the agent"
}reason→followup_messageadditional_context,user_message,decision→ dropped + warn
Observational: afterShellExecution, afterMCPExecution, afterFileEdit
No response fields accepted. All fields dropped with a single warning.
permission: "ask" regression (2.4.21 onward)
Cursor's permission: "ask" has been broken since 2.4.21 and is still broken on 3.2.16 (last verified). The visible behavior is different across the regression window, but the outcome is the same: the hook author's "ask the user" intent is silently dropped.
| Cursor version | What Cursor does with permission: "ask" |
|---|---|
| Pre-2.4.21 | Honored: shows the user an Allow/Deny prompt |
| 2.4.21 through 2.x | Silently treated as deny |
| 3.x (verified on 3.2.16) | Silently treated as allow (the command runs without a prompt) |
The adapter reads cursor_version from stdin and downgrades decision: "ask" to "deny" on every version >= 2.4.21, because that's the safer default in both eras. The stderr warning:
[agent-hooks-bridge] cursor 3.2.16: 'permission: "ask"' is not honored (silently treated as 'deny' on 2.4.21-2.x, 'allow' on 3.x; last verified on 3.2.16); downgrading to 'deny'. Pass cursorAskFallback: "ask" to opt out.To opt out and let ask reach Cursor unchanged (it will still be silently mishandled, but the bridge stays out of the way):
import { run } from "@pivanov/agent-hooks-bridge";
await run(hooks, { cursorAskFallback: "ask" });Pre-2.4.21 Cursor is unaffected: ask passes through verbatim.
Round-trip example
Stdin from Cursor (note hook_event_name: "beforeShellExecution"):
{
"hook_event_name": "beforeShellExecution",
"conversation_id": "conv-xyz",
"cursor_version": "0.46.0",
"command": "rm -rf /tmp/foo",
"cwd": "/Users/x/proj",
"sandbox": false
}The same handler from the getting-started guide returns:
{
decision: "deny",
reason: "rm -rf is blocked"
}Stdout (consumed by Cursor):
{
"permission": "deny",
"agent_message": "rm -rf is blocked"
}exit code: 2
Generated config
.cursor/hooks.json:
{
"version": 1,
"hooks": {
"sessionStart": [
{
"command": "./.hooks/format.ts --host cursor"
}
],
"preToolUse": [
{
"command": "./.hooks/format.ts --host cursor"
}
],
"postToolUse": [
{
"command": "./.hooks/format.ts --host cursor"
}
],
"beforeSubmitPrompt": [
{
"command": "./.hooks/format.ts --host cursor"
}
],
"stop": [
{
"command": "./.hooks/format.ts --host cursor"
}
]
}
}The install command wires preToolUse and postToolUse (the generic events), not the specific ones. Cursor fires the generic events for all tool subevents.