# hooks-prohibited All hooks are prohibited unless explicitly allowlisted; catches new or unexpected hooks added to a project | | | |---|---| | **Severity** | error (disabled) | | **Autofix** | - | | **Since** | v0.12.0 | | **Category** | [Hooks](hooks.md) | ## Why Hooks execute arbitrary shell commands with no human review on every matching event. In high-security environments, any hook that was not explicitly reviewed and allowlisted represents an uncontrolled execution vector — even legitimate hooks should be inventoried. This rule inventories hooks in plugin `hooks/hooks.json` (Claude, Codex and Grok Build, including Codex's and Grok's manifest-declared and inline hooks), APM's compiled copy, `.claude/settings*.json`, **skill/agent frontmatter** (`hooks:` key), `/.codex/hooks.json` and any package's `.codex/hooks.json`, the `[hooks]` tables of a `.codex/config.toml`, `.muse/hooks.json`, Grok Build's `.grok/hooks/*.json`, Cursor's `.cursor/hooks.json`, and Google Antigravity's `hooks.json` in a customization root (`.agents/`, `.agent/`, `_agents/`, `_agent/`) or in one of its plugins. Not every hook spawns a process. Claude Code also dispatches `http`, `mcp_tool`, `prompt` and `agent` handlers, and Codex dispatches `mcp_tool` ones; each fires on the same lifecycle events and each is inventoried here. A handler is inventoried whichever host's file it sits in — an `http` handler in `.muse/hooks.json` is reported even though Muse runs only `command` handlers, because the entry is in the repository and a reviewer reads the file. Whether a given host actually dispatches it is what that host's shape rule (`muse-hooks-valid`, `grok-hooks-valid`, `cursor-hooks-valid`, `antigravity-hooks-valid`) reports. A prompt handler is named by its text: `prompt:` in the nested shape Claude Code defines, and the same spelling for a Cursor `type: "prompt"` entry in the flatter `.cursor/hooks.json` — so one allowlist entry covers a prompt whichever host's file it sits in. ## Examples **Bad (no allowlist configured):** ```json { "hooks": { "PostToolUse": [ {"hooks": [{"type": "command", "command": "scripts/format.sh"}]} ] } } ``` **Good (with allowlist):** ```yaml # .skillsaw.yml rules: hooks-prohibited: allowlist: - "scripts/format.sh" ``` ## How to fix Review the flagged hook and, if it is safe, add it to the `allowlist` in your skillsaw config. Entries match the spelling shown in the diagnostic exactly. This rule is disabled by default — enable it for supply-chain-sensitive repositories. For a `command` hook that spelling is the command itself. For an exec-form hook it joins `command` and `args` with spaces; it does not preserve argument boundaries, so allowlisting only the executable does not permit arbitrary arguments passed to it. A hook that runs no command is named by an identity built from the fields that say what it invokes: | Handler `type` | Allowlist entry | | --- | --- | | `mcp_tool` | `mcp_tool:/` | | `http` | `http:` | | `prompt` | `prompt:` | | `agent` | `agent:` | A handler missing those fields falls back to its bare type — `http` on its own, say. That entry permits every payload-less `http` handler in the repository, so fix the handler rather than allowlisting it; its host's shape rule (`claude-hooks-valid`, `codex-hooks-valid`) reports the missing field. ```yaml # .skillsaw.yml rules: hooks-prohibited: allowlist: - "scripts/format.sh" - "mcp_tool:linter/format" - "http:https://ci.example.com/hooks/post-tool-use" ``` ## Configuration ```yaml rules: hooks-prohibited: enabled: false # true | false | auto severity: error ``` | Parameter | Description | Default | |-----------|-------------|---------| | `allowlist` | Hook spellings to permit (exact diagnostic match): a command, or an identity such as 'mcp_tool:server/tool' for a handler that runs no command | `[]` | *Run `skillsaw explain hooks-prohibited` to see this documentation and the rule's effective configuration in your terminal.*