How Do I Set Up Claude Code Hooks That Block Broken Commits Automatically?

Claude Code is great until it isn't. You ask it to "add the new field to the model" and it adds the field, skips the migration, and leaves a broken test suite in its wake. Hooks turn that from a recurring annoyance into a structural impossibility: Claude can't leave your repo in a broken state because hooks won't let the commit happen. Here's the setup that made my "did Claude forget to run tests again?" moments disappear.
Why this matters
A CLAUDE.md file tells Claude what to do. A hook enforces it. Big difference.
Hooks in Claude Code fire on specific events — before a file edit, after a write, before a bash command runs, before a commit. They can modify the behavior, block it, or add extra steps. For quality work, the important ones are:
- pre-commit: Claude can't commit if tests are broken or lint fails.
- post-edit: the formatter runs on every file Claude touches.
- pre-bash: dangerous commands (
rm -rf,git push --force) require explicit re-approval.
Set these up once, never think about them again. Claude stays disciplined without you being the discipline.
Before you start
You need:
- Claude Code installed with a real project.
- Your project's existing quality tooling — Prettier/Biome, ESLint/TSC, your test runner. Hooks orchestrate these; they don't replace them.
- 10 minutes to set up the three hooks below.
Step 1: Create the hooks directory
In your project root:
mkdir -p .claude/hooksHooks are just scripts. Claude Code looks for a .claude/hooks/<event>.sh (or .ts, .py — any executable) and runs it when the event fires. Shell scripts are the most common because they're the least-surprising to debug.
Step 2: Add a post-edit formatter
.claude/hooks/post-edit.sh:
#!/usr/bin/env bash
set -euo pipefail
FILE="$1" # Claude passes the edited file path as first arg
# Only format files we actually have tooling for
case "$FILE" in
*.ts|*.tsx|*.js|*.jsx|*.json|*.md)
npx prettier --write "$FILE" 2>/dev/null || true
;;
esacMake it executable:
chmod +x .claude/hooks/post-edit.shEvery time Claude edits a file, Prettier formats it. You'll never again review a PR and notice Claude used single quotes where your project uses double.
Step 3: Add a pre-commit test gate
.claude/hooks/pre-commit.sh:
#!/usr/bin/env bash
set -euo pipefail
echo "Running typecheck..."
npx tsc --noEmit
echo "Running linter..."
npm run lint
echo "Running tests..."
npm test -- --run
echo "Pre-commit checks passed."chmod +x .claude/hooks/pre-commit.shIf any of these fail, the commit fails. Claude sees the failure in its output and — usually — tries to fix it. If Claude can't fix it within a few attempts, it will stop and ask you. Either way: broken code never lands.
Important: this is the Claude Code pre-commit hook, not git's. They can coexist (and should — git's pre-commit catches cases where a human bypasses Claude). The Claude hook fires when Claude tries to commit; the git hook fires for everyone.
Step 4: Add a pre-bash safety gate
.claude/hooks/pre-bash.sh:
#!/usr/bin/env bash
set -euo pipefail
CMD="$1" # The command Claude wants to run
# Block the obviously destructive
BLOCKED=(
"rm -rf /"
"rm -rf ~"
"git push --force"
"git push -f"
"DROP DATABASE"
)
for pattern in "${BLOCKED[@]}"; do
if [[ "$CMD" == *"$pattern"* ]]; then
echo "BLOCKED by pre-bash hook: $pattern"
exit 1
fi
done
exit 0This is a last-resort safety net, not a replacement for Claude's built-in dangerous-command prompts. Keep the built-in prompts on too.
Step 5: Verify hooks are firing
Edit a file through Claude. Ask Claude to change a line in src/something.ts. After the edit, check the file — it should be freshly Prettier-formatted even if Claude wrote it unformatted.
Commit through Claude. Ask Claude to commit a change. Before the commit fires, you should see the typecheck/lint/test output streaming in Claude's response. If you don't, the hook didn't run — check file permissions (ls -la .claude/hooks/) and the filename (must match pre-commit.sh exactly, or the event name your Claude Code version uses).
Step 6: Share with the team
Commit .claude/hooks/ to your repo. Every teammate who pulls the repo now has the same hooks.
Add a one-liner to your project README:
This project uses Claude Code hooks in
.claude/hooks/. They enforce formatting and pre-commit quality gates. If a commit unexpectedly fails, check the hook output first.
Consistent hooks across a team mean Claude behaves the same way in everyone's session. That's how you stop PR reviews full of "oh, Claude formatted this weird" nits.
Verify it worked
1. Post-edit formatter runs. Edit any TS file via Claude, check it came back formatted. Try a deliberately ugly edit like const x={a:1,b:2}; — Prettier should produce const x = { a: 1, b: 2 };.
2. Pre-commit gate blocks bad commits. Introduce a type error on purpose, ask Claude to commit. The commit should fail with the typecheck error. Fix, retry — commit succeeds.
3. Pre-bash gate blocks dangerous commands. Ask Claude to run git push --force. The hook should block. Claude should report the block and ask how to proceed.
Where this breaks
- Slow hooks slow every interaction. A pre-commit that takes 3 minutes to run tests means every Claude commit takes 3 minutes. Make hooks fast — split heavy test runs into a separate CI job and keep the hook to typecheck + lint + a smoke test.
- Hook script has a bug. If the hook itself crashes, Claude can't commit anything. Test the hook manually (
.claude/hooks/pre-commit.sh) before trusting it. - Platform-specific scripts.
bashshell scripts work on macOS and Linux. For Windows contributors, use cross-platform tooling (Node scripts, ornpm runwrappers) instead of raw shell. - Hooks that swallow errors silently. A
prettier --write "$FILE" || truehides Prettier errors. You want the|| truein cases where the file shouldn't be formatted (e.g., unsupported extension), but not in cases where Prettier legitimately failed. Log the error even when you don't fail on it. - Forgetting to update hooks when tooling changes. When the project switches from ESLint to Biome, the pre-commit hook still calls ESLint and silently no-ops. Pin hooks to the current tooling and update in the same PR as the tooling change.
What to try next
- How Do I Write a CLAUDE.md That Actually Changes Claude's Behavior? — hooks enforce mechanically, CLAUDE.md nudges behaviorally. Use both.
- How Do I Schedule Claude Code to Run Overnight Jobs? — hooks matter even more for unattended runs. No human watching means quality gates are your only guardrail.
- How Do I Install and Publish Claude Code Plugins from the Marketplace? — package well-tested hook sets as plugins other teams can install with one command.
Let's talk about your AI + SEO stack
If you'd rather skip the how-to and have it shipped for you, that's what I do. Start a conversation and we'll figure out the fastest path to results.
Let's Talk