| name | hookfrisk |
| description | Frisk your agent hooks for commands that auto-run on untrusted input before you ship them. Triggers automatically when you are about to report a coding task done and your diff added or changed a hook (or on /hookfrisk). A hook fires by itself, with no human in the loop — so a hook that pipes tool output or a file path into a shell, or runs curl | bash, is remote code execution on every trigger. hookfrisk reads only the changed hook definitions, flags the ones that run attacker-influenced input or unsandboxed commands, proposes the safe rewrite, and refuses to say "done" while an auto-firing hook can be turned against you. |
hookfrisk — no auto-run hook ships unsafe
Hooks are the one part of an agent setup that runs on its own — on every tool
call, every commit, every trigger word — with no confirmation prompt. That's the
whole point of them, and also the whole danger: the input a hook runs on is often
attacker-reachable (a model's tool output, a filename in the repo, a webhook body,
a spoken phrase). A PostToolUse hook that pipes $TOOL_OUTPUT into sh, or a
git hook that runs a script fetched over the network, is a standing RCE that fires
without anyone watching. The agent wiring the hook is the last place to catch it,
because after it ships there is no click to intercept.
When to run
- Automatically, right before you report a coding task complete, if the diff
added or changed a hook: Claude Code hooks in
settings.json/.claude
(PreToolUse, PostToolUse, Stop, UserPromptSubmit, SessionStart), git
hooks (.git/hooks, .githooks, core.hooksPath, Husky/Lefthook/pre-commit),
agent trigger-command / voice-hook configs, CI on:-triggered run steps that
execute repo-supplied input.
- On demand when the user types
/hookfrisk (audits the current diff, or a
named hook file).
What it looks at
Only the hooks touched by the diff — the command a hook runs and the input it
runs on. Don't audit the whole config; stay scoped. The key question for each
hook: where does its input come from, and can an attacker influence it?
The process
- Untrusted input into a shell (HIGH — this is the core one). The hook
interpolates a value an attacker can control into a shell command: model/tool
output (
$TOOL_OUTPUT, $CLAUDE_TOOL_INPUT, a PostToolUse payload), a
filename or file contents from the repo, a commit message, a webhook/voice
transcript. Piping any of those into sh/bash/eval/backticks = command
injection that fires automatically. Trace the variable to its source.
- Fetch-and-execute (HIGH).
curl … | bash, wget … | sh, npx <remote>,
sourcing a script from a URL or a path the repo can write. The hook runs code
it doesn't contain — supply-chain RCE on every trigger.
- Over-broad auto-run (HIGH–MED). A hook with no allow-list that will run
any command the model emits (an "auto-approve" or "run whatever" PreToolUse),
rm -rf/destructive ops on a path built from input, secrets read into the
command line (visible in ps/logs), a hook that writes outside its workdir.
- No fail-safe (MED–LOW). A hook with no timeout (a hang blocks the agent),
no error handling (a non-zero exit corrupts the session), or one that runs on a
broader event than it needs (
SessionStart when PreToolUse on one tool would
do). A hook that swallows its own errors silently.
- Downgrade honestly. A hook that runs a fixed command on no external input
(a formatter, a linter, a static notify), reads-only, or is gated behind an
explicit allow-list is fine. Say so and move on — hooks are useful; the target
is the ones that eval untrusted input.
What to do with findings
- Do the safe, mechanical fix yourself when it's clear: pass the untrusted
value as an argument (
sh -c 'cmd "$1"' _ "$VALUE") or via stdin instead of
interpolating it into the command string; replace curl | bash with a pinned,
vendored, checksum-verified script; add an allow-list of permitted commands; add
a timeout and an error gate; narrow the trigger event. State the fix.
- Escalate anything that needs intent: whether an auto-approve hook should
exist at all, whether a powerful command is acceptable for this workflow, a
trust decision about a remote source. Describe the risk, give 1–2 options, ask
approve / fix / skip.
- Never invent a finding. A fixed-command formatter hook is not a
vulnerability. False alarms train people to ignore you and keep the risky hooks.
The hard rule
Do not report the task as done while a hook in the diff auto-runs
attacker-influenceable input through a shell, or fetches-and-executes remote code,
and it's neither fixed nor explicitly accepted by the user. If every changed hook
runs a fixed, trusted command or is gated, say so in one line and finish.
Output format
hookfrisk — N changed hook(s)
✗ HIGH settings.json PostToolUse pipes $TOOL_OUTPUT into `sh -c` → model output = shell injection; pass as argv instead [fixed]
✗ HIGH .githooks/pre-push:4 curl https://… | bash → remote RCE on every push — vendor + pin, or your call [escalated]
⚠ MED settings.json PreToolUse no timeout; a hang blocks the agent → added 10s timeout [fixed]
✓ .githooks/pre-commit runs prettier on staged files, fixed command — fine
1 hook needs your call before this is done.
Be terse. Real signal only. Severity is HIGH whenever an anonymous or model-
controlled value reaches a shell, or remote code is fetched-and-run; MED for
missing fail-safes and over-broad triggers; LOW for hardening.