
luijhy michael guerra floresClaude Code is very good at running commands. That's exactly the problem when it runs the wrong one....
Claude Code is very good at running commands. That's exactly the problem when it runs the wrong one. Hooks let you add a safety net that runs before every action, without relying on the model "remembering" your rules.
.claude/settings.json, per event (PreToolUse, PostToolUse, Notification…) and per tool (Bash, Write|Edit…)..claude/hooks/block-dangerous-commands.sh:
#!/usr/bin/env bash
input="$(cat)"
patterns=(
'git[^;&|]*[[:space:]]push([[:space:]][^;&|]*)?[[:space:]](--force([^-]|$)|-[a-zA-Z]*f[a-zA-Z]*([[:space:]"]|$)|\+[^[:space:]"]+)'
'git[^;&|]*[[:space:]]reset([[:space:]][^;&|]*)?[[:space:]]--hard'
'DROP[[:space:]]+(TABLE|DATABASE)'
'(curl|wget)[^|]*\|[[:space:]]*(sudo[[:space:]]+)?(ba)?sh'
)
for pattern in "${patterns[@]}"; do
if printf '%s' "$input" | grep -qiE -- "$pattern"; then
echo "Blocked: matches a dangerous pattern. Ask the user to run it." >&2
exit 2
fi
done
exit 0
Two details in the push pattern: --force([^-]|$) blocks --force but lets --force-with-lease through, which is the safe way to force-push; and \+[^[:space:]"]+ catches a + refspec (git push origin +main), which forces without ever typing --force. The git[^;&|]* prefix also catches git -C some/dir push --force and git reset -q --hard.
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous-commands.sh" }
]
}
]
}
}
Don't forget chmod +x, then test it:
echo '{"tool_input":{"command":"git push origin +main"}}' | .claude/hooks/block-dangerous-commands.sh; echo $?
# → 2
I tested it in a real session: I asked Claude to run git push --force origin main and it came back with "blocked by a PreToolUse hook, run it yourself if you really need it".
Defense in depth: a regex hook is an early warning, not a guarantee. A reader of the Spanish version of this post found two forced-push variants that slipped past my first patterns (both fixed above). For anything irreversible, put the real block where the agent can't edit it: branch protection on the remote with force pushes disabled. If the regex falls short, the push bounces off the server.
Same pattern with matcher: "Write|Edit|MultiEdit" and regexes like AKIA[0-9A-Z]{16} (AWS), sk-ant-[A-Za-z0-9_-]{20,} (Anthropic) or gh[pousr]_[A-Za-z0-9]{36,} (GitHub). When Claude tries to write a key, it gets "use an environment variable and document it in .env.example" and fixes it on its own.
A PostToolUse hook that reads file_path from the JSON and runs the project's own Prettier/Biome (node_modules/.bin/prettier). It always exits 0: it never blocks, it just tidies.
Hooks 1 and 2, with 27 tests running on Linux and macOS, are on GitHub under MIT: https://github.com/kailucho/claude-code-hooks-seguridad (docs in Spanish, English summary at the top).
The Starter Kit for Claude Code (US$9, unofficial) bundles the three hooks plus a notifications one, 6 skills (/commit, /pr, /review, /tests, /plan, /debug), CLAUDE.md templates for React, Node and GraphQL, and permission presets: https://luijhy.gumroad.com/l/starter-kit-for-claude-code
What other command would you block? Let me know in the comments.