Skip to content

cursor-hooks-valid

.cursor/hooks.json must declare version 1 and known hook events with commands

Severity error (auto)
Autofix -
Since v0.19.0
Repo Types cursor
Category Cursor

Why

.cursor/hooks.json runs shell commands around the agent loop — before a shell command executes, before an MCP tool call, after a file edit. It ships in the repository, so anyone who can land a commit can add one.

A key Cursor does not dispatch is ignored: the file loads, the hook never fires, and nothing is reported. A hook meant to block dangerous shell commands that is spelled beforeShellExec is not a hook at all, and the failure looks identical to a hook that simply never triggered.

Cursor's event set grows — the 1.7 launch shipped six, and there are now over twenty across agent, Tab, and application lifecycle. An unrecognised name is therefore reported at warning, not error, and extra-events lets a project accept an event newer than its skillsaw without waiting for a release.

A hook entry is a command hook by default. A type: "prompt" hook asks the model a question instead of spawning a process, and carries its text in prompt rather than command.

This rule checks the shape. The commands themselves are scanned by hooks-dangerous and, when you want every hook reviewed rather than only the risky-looking ones, hooks-prohibited — both read Cursor hooks through the same path they use for Claude Code hooks and settings.

Severity

Structural defects that stop a hook running are errors: a missing or non-integer version, a missing hooks object, an event whose value is not an array, an entry that is not an object, an unknown type, a missing or empty command/prompt, a non-string matcher, and a timeout that is not a finite number.

A bad matcher is worth the error even though the hook still runs: skillsaw falls back to the .* wildcard so the security rules keep seeing the command, which means the hook fires on everything and nothing else would tell you.

Three checks are warnings, because the file still loads and the rest of it still runs: an unrecognised event name, an empty hooks object, and an event whose array is empty and so configures nothing.

Examples

Bad — a typo'd event that never fires, and a hook with nothing to run:

{
  "version": 1,
  "hooks": {
    "beforeShellExec": [{ "command": "./scripts/audit.sh" }],
    "afterFileEdit": [{ "command": "" }]
  }
}

Good — a command hook and a prompt hook:

{
  "version": 1,
  "hooks": {
    "beforeShellExecution": [
      { "command": "./scripts/audit-shell.sh" },
      { "type": "prompt", "prompt": "Does this command look safe?", "timeout": 10 }
    ],
    "afterFileEdit": [{ "command": "./scripts/format.sh" }]
  }
}

How to fix

  • Correct the event name to one Cursor dispatches. If Cursor added it after this skillsaw release, list it under the rule's extra-events setting:
rules:
  cursor-hooks-valid:
    extra-events:
      - afterSomethingNew
  • Give every command hook a non-empty command: an absolute path, a path relative to the project root, or a shell snippet. For a script stored beside the manifest, use .cursor/hooks/script.sh, not ./hooks/script.sh. Give every prompt hook a non-empty prompt.
  • Set "version": 1 — it is required, and 1 is the only value Cursor accepts today. Write it unquoted; "1" is a string.

Configuration

rules:
  cursor-hooks-valid:
    enabled: auto  # true | false | auto
    severity: error
Parameter Description Default
extra-events Additional hook event names to accept, for events newer than this skillsaw release []

Run skillsaw explain cursor-hooks-valid to see this documentation and the rule's effective configuration in your terminal.