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):
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¶
| 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.