# 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](cursor.md) | ## 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`](hooks-dangerous.md) and, when you want every hook reviewed rather than only the risky-looking ones, [`hooks-prohibited`](hooks-prohibited.md) — 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: ```json { "version": 1, "hooks": { "beforeShellExec": [{ "command": "./scripts/audit.sh" }], "afterFileEdit": [{ "command": "" }] } } ``` **Good** — a command hook and a prompt hook: ```json { "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: ```yaml 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 ```yaml 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.*