Package Exports
- cursor-hooks
- cursor-hooks/package.json
Readme
cursor-hooks
TypeScript definitions and helpers for building Cursor agent hooks.
Installation
1. Create a hooks directory in your project
mkdir -p .cursor/hooks
cd .cursor/hooks2. Initialize a Bun project
Install Bun if you haven't already, then initialize:
bun init -y3. Install cursor-hooks
bun install cursor-hooks4. Create a hooks.json configuration
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.
{
"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.
5. Enable JSON Schema validation (optional)
Add this to your Cursor settings (~/Library/Application Support/Cursor/User/settings.json on macOS) to get autocomplete and validation:
{
"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"
}
]
}Real-world Examples
Example 1: Block npm commands, enforce bun
before-shell-execution.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));Example 2: Gate prompts with a keyword
before-submit-prompt.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));Example 3: Auto-format and log file edits
after-file-edit.ts:
import type { AfterFileEditPayload } from "cursor-hooks";
import { appendFile, readFile, writeFile } from "node:fs/promises"
import { join } from "node:path"
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,
}
console.error(JSON.stringify(output, null, 2))
const logFilePath = join(input.workspace_roots[0]!, "logs", "after-file-edit.jsonl")
// Check if log file exists and count entries
let logContent = ""
try {
logContent = await readFile(logFilePath, "utf-8")
} catch (_error) {
// File doesn't exist yet, that's fine
}
// Count JSON objects by counting opening braces at the start of lines
const entryCount = (logContent.match(/^\s*\{/gm) || []).length
// If we have more than 500 entries, remove the first 100
if (entryCount >= 500) {
const lines = logContent.trim().split('\n')
const jsonObjects: string[] = []
let currentObject = ""
let braceCount = 0
// Parse JSON objects from the content
for (const line of lines) {
currentObject += `${line}\n`
braceCount += (line.match(/\{/g) || []).length
braceCount -= (line.match(/\}/g) || []).length
if (braceCount === 0 && currentObject.trim()) {
jsonObjects.push(currentObject.trim())
currentObject = ""
}
}
// Keep only the last (total - 100) entries
const entriesToKeep = jsonObjects.slice(100)
const trimmedContent = `${entriesToKeep.join('\n')}\n`
await writeFile(logFilePath, trimmedContent)
}
await appendFile(logFilePath, JSON.stringify(output, null, 2))
}Available Hook Types
beforeShellExecution
Intercept and control shell commands before execution.
Payload: BeforeShellExecutionPayload
command: The shell command to executeworkspace_roots: Array of workspace root pathshook_event_name: "beforeShellExecution"
Response: BeforeShellExecutionResponse
permission: "allow" | "deny" | "ask"agentMessage?: Message shown to the AI agentuserMessage?: Message shown to the user
beforeSubmitPrompt
Control whether prompts are submitted to the AI.
Payload: BeforeSubmitPromptPayload
prompt: The user's prompt textworkspace_roots: Array of workspace root pathshook_event_name: "beforeSubmitPrompt"
Response: BeforeSubmitPromptResponse
continue: boolean - whether to allow the prompt
afterFileEdit
React to file edits made by the AI agent.
Payload: AfterFileEditPayload
file_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)
beforeReadFile
Control file access before the AI reads a file.
Payload: BeforeReadFilePayload
file_path: Path to the file being readcontent: The file contentsworkspace_roots: Array of workspace root pathshook_event_name: "beforeReadFile"
Response: BeforeReadFileResponse
permission: "allow" | "deny"agentMessage?: Message shown to the AI agentuserMessage?: Message shown to the user
beforeMCPExecution
Control MCP tool execution before it runs.
Payload: BeforeMCPExecutionPayload
tool_name: Name of the MCP toolarguments: Tool argumentsworkspace_roots: Array of workspace root pathshook_event_name: "beforeMCPExecution"
Response: BeforeMCPExecutionResponse
permission: "allow" | "deny" | "ask"agentMessage?: Message shown to the AI agentuserMessage?: Message shown to the user
stop
Notification when the AI agent finishes execution.
Payload: StopPayload
status: The completion statusworkspace_roots: Array of workspace root pathshook_event_name: "stop"
Response: None (this is a notification hook)
Type Safety Helpers
isHookPayloadOf
Validate and narrow hook payload types:
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);
}JSON Schema Validation
Add schema validation to your hooks.json:
{
"$schema": "https://unpkg.com/cursor-hooks@latest/schema/hooks.schema.json",
"version": 1,
"hooks": {
"afterFileEdit": [
{ "command": "bun run .cursor/hooks/format.ts" }
]
}
}Development
Build
npm run buildRelease
- Follow Conventional Commits for automatic versioning
- CI automatically publishes to npm on pushes to
main - Run
./scripts/setup-publish.shto configure npm token
License
MIT