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