DEV Community

Cover image for 3 Claude Code hooks that prevent disasters (with code)

3 Claude Code hooks that prevent disasters (with code)

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.

How they work (in 30 seconds)

  • They're configured in .claude/settings.json, per event (PreToolUse, PostToolUse, Notification…) and per tool (Bash, Write|Edit…).
  • The hook receives the event as JSON on stdin.
  • If it exits with code 2, the action is blocked and the stderr text is sent back to Claude as the reason.

1. Block destructive commands

.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
Enter fullscreen mode Exit fullscreen mode

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" }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Don't forget chmod +x, then test it:

echo '{"tool_input":{"command":"git push origin +main"}}' | .claude/hooks/block-dangerous-commands.sh; echo $?
# → 2
Enter fullscreen mode Exit fullscreen mode

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.

2. Catch secrets before they're written

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.

3. Format after every edit

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.

Free code

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

If you want it all wired up

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.

Top comments (0)