Guard Claude Code against destructive commands with a fail-closed hook

A developer's Claude Code guard hook flagged 9 risky commands in three weeks and stopped none, because it exited 1. After switching to exit 2 it blocked 40 of 40 in a replay.

Done withClaude Code· Oct 18, 2026

What they did

jidonglab, a developer who builds an interview prep platform, put a small Python script in front of Claude Code's Bash tool as a PreToolUse hook. It read each tool call, matched the command against three patterns (rm -rf, force push, git reset --hard) and printed a warning when one matched. It ran for three weeks during normal coding sessions. After Claude ran git reset --hard and wiped an afternoon of uncommitted work, the author searched three weeks of session transcripts for the warning, then replayed 40 dangerous commands through Claude Code in a throwaway repo against three versions of the hook: the original, one that exits with code 2, and the fixed one with a deliberate crash bug.

How it went

The warning appeared 9 times and all 9 commands ran: five rm -rf on build folders, three force pushes (one to a shared branch, which deleted a teammate's commit) and the reset. The hook exited 1, which Claude Code treats as a non-blocking error. In the replay the original blocked 0 of 40, the exit 2 version blocked 40 of 40, and the crashing version blocked 0. With the fixed hook Claude tried another route 6 times; twice it deleted a folder with find -delete or a Python call that the patterns did not cover.

Worth knowing

Only exit code 2 blocks a PreToolUse call; any crash, missing tool or other non-zero code lets the command through, so wrap the script to exit 2 on any exception. The matcher is case-sensitive, so bash does not match Bash. A pattern list is easy to route around; the author also relies on settings deny rules and protected branches in git.

Try it

With Claude Code· 2 parts to fill in

Write a PreToolUse hook for the Bash tool in [settings file] that refuses commands matching [list of dangerous patterns]. On a match, exit with code 2 and print a message telling you to ask me first. Wrap the whole script so any crash or bad input also exits 2. Then test it without running a session: pipe fake tool-call JSON for each pattern, for a safe command and for garbage input, and show me each exit code. Stop and show me the script and results before registering it.

Square brackets are the parts to fill in. The prompt is ours; the story is theirs.

Where they said it

Related