Skip to content

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

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), <repo>/.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:<text> 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):

{
  "hooks": {
    "PostToolUse": [
      {"hooks": [{"type": "command", "command": "scripts/format.sh"}]}
    ]
  }
}

Good (with allowlist):

# .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:<server>/<tool>
http http:<url>
prompt prompt:<prompt>
agent agent:<prompt>

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.

# .skillsaw.yml
rules:
  hooks-prohibited:
    allowlist:
      - "scripts/format.sh"
      - "mcp_tool:linter/format"
      - "http:https://ci.example.com/hooks/post-tool-use"

Configuration

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.