Type definitions and helpers for building Cursor agent hooks.
npm install cursor-hooksTypeScript definitions and helpers for building Cursor agent hooks.
``bash`
mkdir -p .cursor/hooks
cd .cursor/hooks
Install Bun if you haven't already, then initialize:
`bash`
bun init -y
`bash`
bun install cursor-hooks
In your .cursor directory (at the project root), create a hooks.json file.
Important: Hook command paths are relative to the .cursor/hooks.json file location.
`json`
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"command": "bun run hooks/before-shell-execution.ts"
}
],
"afterFileEdit": [
{
"command": "bun run hooks/after-file-edit.ts"
}
]
}
}
This configuration assumes your hooks are in .cursor/hooks/ and the hooks.json is at .cursor/hooks.json.
You can enable schema validation in two ways:
Option A: Add $schema directly to your hooks.json:
`json`
{
"$schema": "https://unpkg.com/cursor-hooks@latest/schema/hooks.schema.json",
"version": 1,
"hooks": {
"afterFileEdit": [
{ "command": "bun run hooks/after-file-edit.ts" }
]
}
}
Option B: Configure it globally in your Cursor settings (~/Library/Application Support/Cursor/User/settings.json on macOS):
`json`
{
"json.validate.enable": true,
"json.format.enable": true,
"json.schemaDownload.enable": true,
"json.schemas": [
{
"fileMatch": [".cursor/hooks.json"],
"url": "https://unpkg.com/cursor-hooks/schema/hooks.schema.json"
}
]
}
before-shell-execution.ts:
`ts
import type { BeforeShellExecutionPayload, BeforeShellExecutionResponse } from "cursor-hooks";
const input: BeforeShellExecutionPayload = await Bun.stdin.json();
const startsWithNpm = input.command.startsWith("npm") || input.command.includes(" npm ");
const output: BeforeShellExecutionResponse = {
permission: startsWithNpm ? "deny" : "allow",
agentMessage: startsWithNpm ? "npm is not allowed, always use bun instead" : undefined,
};
console.log(JSON.stringify(output, null, 2));
`
before-submit-prompt.ts:
`ts
import type { BeforeSubmitPromptPayload, BeforeSubmitPromptResponse } from "cursor-hooks";
const input: BeforeSubmitPromptPayload = await Bun.stdin.json();
const output: BeforeSubmitPromptResponse = {
continue: input.prompt.includes("allow"),
};
console.log(JSON.stringify(output, null, 2));
`
after-file-edit.ts:
`ts
import type { AfterFileEditPayload } from "cursor-hooks";
const input: AfterFileEditPayload = await Bun.stdin.json();
if (input.file_path.endsWith(".ts")) {
const result = await Bun.$bunx @biomejs/biome lint --fix --unsafe --verbose ${input.file_path}
const output = {
timestamp: new Date().toISOString(),
...input,
stdout: result.stdout.toString(),
stderr: result.stderr.toString(),
exitCode: result.exitCode,
}
// only console.errors show in the logs
console.error(JSON.stringify(output, null, 2))
}
`
Intercept and control shell commands before execution.
Payload: BeforeShellExecutionPayloadcommand
- : The shell command to executeworkspace_roots
- : Array of workspace root pathshook_event_name
- : "beforeShellExecution"
Response: BeforeShellExecutionResponsepermission
- : "allow" | "deny" | "ask"agentMessage?
- : Message shown to the AI agentuserMessage?
- : Message shown to the user
Control whether prompts are submitted to the AI.
Payload: BeforeSubmitPromptPayloadprompt
- : The user's prompt textworkspace_roots
- : Array of workspace root pathshook_event_name
- : "beforeSubmitPrompt"
Response: BeforeSubmitPromptResponsecontinue
- : boolean - whether to allow the prompt
React to file edits made by the AI agent.
Payload: AfterFileEditPayloadfile_path
- : Path to the edited fileedits
- : Array of edit operationsworkspace_roots
- : Array of workspace root pathshook_event_name
- : "afterFileEdit"
Response: None (this is a notification hook)
Control file access before the AI reads a file.
Payload: BeforeReadFilePayloadfile_path
- : Path to the file being readcontent
- : The file contentsworkspace_roots
- : Array of workspace root pathshook_event_name
- : "beforeReadFile"
Response: BeforeReadFileResponsepermission
- : "allow" | "deny"agentMessage?
- : Message shown to the AI agentuserMessage?
- : Message shown to the user
Control MCP tool execution before it runs.
Payload: BeforeMCPExecutionPayloadtool_name
- : Name of the MCP toolarguments
- : Tool argumentsworkspace_roots
- : Array of workspace root pathshook_event_name
- : "beforeMCPExecution"
Response: BeforeMCPExecutionResponsepermission
- : "allow" | "deny" | "ask"agentMessage?
- : Message shown to the AI agentuserMessage?
- : Message shown to the user
Notification when the AI agent finishes execution.
Payload: StopPayloadstatus
- : The completion statusworkspace_roots
- : Array of workspace root pathshook_event_name
- : "stop"
Response: None (this is a notification hook)
Validate and narrow hook payload types:
`ts
import { isHookPayloadOf } from "cursor-hooks";
const rawInput: unknown = await Bun.stdin.json();
if (isHookPayloadOf(rawInput, "beforeShellExecution")) {
// TypeScript now knows rawInput is BeforeShellExecutionPayload
console.log(rawInput.command);
}
`
`bash`
bun run build
- Follow Conventional Commits for automatic versioning
- CI automatically publishes to npm on pushes to main./scripts/setup-publish.sh` to configure npm token
- Run
MIT