Verified against Claude Code · 2026-07-24
Write a PreToolUse hook that blocks a dangerous command before Claude Code runs it
A prompt that generates a Claude Code PreToolUse hook — matcher pattern, hook script contract, and exit-code semantics — that mechanically blocks a specific risky action instead of relying on a CLAUDE.md rule the agent could still reason its way past.
The prompt
Ready to copy — highlighted parts are example details you can swap.
Write a Claude Code PreToolUse hook that mechanically blocks a force-push to any protected branch before it executes, rather than relying on a CLAUDE.md instruction the agent could still talk itself past under pressure to finish a task. WHAT MUST NEVER RUN git push --force or git push -f targeting main, master, or any release/* branch, from any working directory in this repo. WHAT SHOULD STILL BE ALLOWED A force-push to a personal feature branch under feature/ must still work normally. PRODUCE 1. The settings.json fragment: a hooks entry under PreToolUse with a matcher narrow enough to catch Bash tool calls whose command string contains push and either --force or -f without also catching unrelated tool calls that happen to share a substring — a matcher that is too broad blocks legitimate work, and one that is too narrow lets a slightly different phrasing of the same dangerous command through. 2. The hook script itself, as a small shell or Python script, that reads the JSON payload from stdin, inspects the tool name and the actual tool input for the dangerous pattern rather than trusting the matcher alone to have caught it precisely, and exits with the code that blocks the call, writing a short, specific reason to stderr — the reason a human, or the agent reading it back, would actually need to understand why this was stopped and what to do instead. 3. A note on exit-code semantics used: which exit code blocks the call and surfaces stderr back to Claude as the reason, versus a nonzero code that only surfaces an error to the human without blocking, and confirm the script uses the blocking one deliberately, not by accident. CONSTRAINTS - The check inside the script must be based on the actual command content in the tool input, not just the tool name — a hook that blocks every Bash call because one specific command is dangerous is a blunt instrument that will also block every safe git status and ls the agent needs to run. - Fail closed, not open: if the hook script itself errors, or the payload is malformed in a way that makes the check inconclusive, the default should be to block and say why, not to silently allow the action through. - Do not make the block message vague. A bare 'this command is not allowed' gives the agent nothing to work with; state the specific pattern matched and, if git push --force-with-lease, which the team already permits on feature branches exists, name it directly so the agent can retry correctly instead of guessing at a workaround. - Test the hook against both a case that should be blocked and a case that looks similar but should be allowed, and show both example payloads and the script's actual exit code for each. OUTPUT The settings.json fragment, the hook script in full, and the two test cases with their outcomes.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
A CLAUDE.md rule against force-pushing to main is an instruction the model reads and, in the overwhelming majority of turns, follows — but it is still text competing with the model's own judgment in that turn, and an agent under pressure to unblock a task it believes is otherwise finished can talk itself into an exception a strict rule was meant to prevent. A PreToolUse hook is a different category of control entirely: it is enforced by Claude Code's runtime before the tool call is allowed to execute, so the question of whether the model currently believes the exception is justified never gets a vote — the call either executes or it does not, based on the hook script's own exit code, independent of any reasoning that happened in the conversation. This is exactly why the exit-code semantics deserve their own explicit section rather than being left implicit: a hook that exits with the wrong code for what it intends produces a guardrail that looks correct in a code review of the script but does nothing in practice, silently downgrading from blocking to a message the human sees while the dangerous action still runs. Failing closed on the hook's own internal error is the same principle applied one level up — a guardrail whose failure mode is to allow the action through by default is not a safety net, it is a safety net with a hole exactly the size of 'when the check itself breaks,' and that is precisely when a genuinely risky action is most likely to be attempted, since it usually means something about the environment or input was already unusual. The instruction to name the specific matched pattern and a safe alternative in the block message is not just good UX — stderr from a blocking PreToolUse hook is fed back into Claude's own context as the reason the call failed, so a specific, actionable message lets the agent immediately retry with the safe alternative in the same turn, while a vague message forces either a human to step in or the agent to guess at a workaround, which is a worse outcome than the hook not existing if the guess is another unsafe attempt.
Verified against
Claude Code Sonnet 4.6 · 2026-07-24
Changelog
- 2026-07-24 — Initial publish, verified against Claude Code hooks (Sonnet 4.6).
Building this for real?
This is a free starting point. If you'd rather have custom software built and running for your business, that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
