Skip to content

cursor-hooks-valid

Cursor hooks must use known events and valid commands; project hooks require version 1

Severity error (auto)
Autofix -
Since v0.19.0
Repo Types cursor, cursor-marketplace, cursor-plugin
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 project-hook version or a 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. A severity configured for the rule applies to these as well.

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 in project hooks — it is required there. Plugin hooks may omit it; when present, 1 is the only accepted value. Write it unquoted; "1" is a string.

Plugin hooks

Native Cursor plugins also load hooks from hooks/hooks.json, a declared file, or an inline hooks object in their manifest or marketplace entry. The same event and handler checks apply. Unlike project .cursor/hooks.json, plugin hooks may omit version; an explicit version is still validated.

A plugin that also ships a Claude Code manifest often keeps Claude's hooks/hooks.json at that default path, with matcher groups that nest a hooks array under PascalCase events. When every entry has that shape, the rule reports one finding for the file instead of checking each entry. Declare hooks in the Cursor manifest or marketplace entry, pointing at a separate Cursor-format file such as hooks/hooks-cursor.json.

That finding is a warning when no manifest or marketplace entry declares hooks and Cursor only picks the file up at its default path. When the author pointed Cursor at the Claude-format file, or wrote Claude-format hooks inline, it keeps the rule's error severity. A severity set in .skillsaw.yaml overrides both.

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.