# cursor-hooks

TypeScript definitions and helpers for building [Cursor agent hooks](https://cursor.com/docs/agent/hooks).

## Installation

### 1. Create a hooks directory in your project

```bash
mkdir -p .cursor/hooks
cd .cursor/hooks
```

### 2. Initialize a Bun project

Install [Bun](https://bun.sh/) if you haven't already, then initialize:

```bash
bun init -y
```

### 3. Install cursor-hooks

```bash
bun install cursor-hooks
```

### 4. 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.

```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`.

### 5. Enable JSON Schema validation (optional)

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"
    }
  ]
}
```

## Real-world Examples

### Example 1: Block npm commands, enforce bun

`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));
```

### Example 2: Gate prompts with a keyword

`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));
```

### Example 3: Auto-format and log file edits

`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))
}
```

## Available Hook Types

### beforeShellExecution

Intercept and control shell commands before execution.

**Payload:** `BeforeShellExecutionPayload`
- `command`: The shell command to execute
- `workspace_roots`: Array of workspace root paths
- `hook_event_name`: "beforeShellExecution"

**Response:** `BeforeShellExecutionResponse`
- `permission`: "allow" | "deny" | "ask"
- `agentMessage?`: Message shown to the AI agent
- `userMessage?`: Message shown to the user

### beforeSubmitPrompt

Control whether prompts are submitted to the AI.

**Payload:** `BeforeSubmitPromptPayload`
- `prompt`: The user's prompt text
- `workspace_roots`: Array of workspace root paths
- `hook_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 file
- `edits`: Array of edit operations
- `workspace_roots`: Array of workspace root paths
- `hook_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 read
- `content`: The file contents
- `workspace_roots`: Array of workspace root paths
- `hook_event_name`: "beforeReadFile"

**Response:** `BeforeReadFileResponse`
- `permission`: "allow" | "deny"
- `agentMessage?`: Message shown to the AI agent
- `userMessage?`: Message shown to the user

### beforeMCPExecution

Control MCP tool execution before it runs.

**Payload:** `BeforeMCPExecutionPayload`
- `tool_name`: Name of the MCP tool
- `arguments`: Tool arguments
- `workspace_roots`: Array of workspace root paths
- `hook_event_name`: "beforeMCPExecution"

**Response:** `BeforeMCPExecutionResponse`
- `permission`: "allow" | "deny" | "ask"
- `agentMessage?`: Message shown to the AI agent
- `userMessage?`: Message shown to the user

### stop

Notification when the AI agent finishes execution.

**Payload:** `StopPayload`
- `status`: The completion status
- `workspace_roots`: Array of workspace root paths
- `hook_event_name`: "stop"

**Response:** None (this is a notification hook)

## Type Safety Helpers

### isHookPayloadOf

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);
}
```

## Development

### Build

```bash
bun run build
```

### Release

- Follow [Conventional Commits](https://www.conventionalcommits.org/) for automatic versioning
- CI automatically publishes to npm on pushes to `main`
- Run `./scripts/setup-publish.sh` to configure npm token

## License

MIT
