Back to guides

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

Jake McCluskeyIntermediate35 min read
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:

bash
mkdir -p .claude/hooks

Hooks 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:

bash
#!/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
    ;;
esac

Make it executable:

bash
chmod +x .claude/hooks/post-edit.sh

Every 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:

bash
#!/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."
bash
chmod +x .claude/hooks/pre-commit.sh

If 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:

bash
#!/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 0

This 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. bash shell scripts work on macOS and Linux. For Windows contributors, use cross-platform tooling (Node scripts, or npm run wrappers) instead of raw shell.
  • Hooks that swallow errors silently. A prettier --write "$FILE" || true hides Prettier errors. You want the || true in 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

Want this built for you instead?

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
Questions from readers

Frequently asked

How are Claude Code hooks different from git hooks?

Git hooks fire when any user runs git. Claude hooks fire when Claude runs operations — on edit, on bash, on commit. They complement each other: Claude hooks enforce inside the Claude session; git hooks enforce for everyone. Use both.

Can hooks slow Claude down a lot?

Only if you write slow hooks. A post-edit Prettier run takes 200ms and nobody notices. A pre-commit full test suite taking 3 minutes will make every commit painful. Keep hooks fast — typecheck and lint on commit, defer full test runs to CI.

What if the hook script itself has a bug?

Claude can't do anything protected by that hook until you fix it. Test hooks by running them manually (./.claude/hooks/pre-commit.sh) before trusting them. Start with a no-op hook, verify the event fires, then add logic.

Do hooks work in unattended scheduled runs?

Yes — hooks fire regardless of whether a human is watching. They're especially valuable for unattended runs because they're the only quality gate. Always pair overnight Claude Code jobs with strong pre-commit hooks.

Can hooks call Claude back?

Yes — hooks are just scripts, they can invoke claude --print to delegate sub-tasks. Useful for 'if the commit fails, run Claude to auto-fix' patterns, though beware of recursion. Set a retry count and bail out if auto-fix doesn't resolve in 2 rounds.