
Catch Bugs Before CI Does: Wire `claude -p` Into a Git Pre-Commit Hook in 15 Lines
Chris Harper
3 min read
Jul 21, 2026 · 12:03 UTC
TL;DR: Pipe git diff --staged into claude -p from a pre-commit hook and get a focused bug/security review on every commit — 15 lines of bash, zero CI cost.
CI catches what lands on main. A pre-commit hook catches what's about to leave your machine. Wiring Claude's headless mode into that hook gives you a fast, opinionated reviewer on every staged diff before you type git push.
The hook
Save this as .git/hooks/pre-commit and make it executable (chmod +x .git/hooks/pre-commit):
#!/usr/bin/env bash
set -euo pipefail
DIFF=$(git diff --staged)
[[ -z "$DIFF" ]] && exit 0 # nothing staged, skip
REVIEW=$(echo "$DIFF" | claude -p "
You are a senior code reviewer. Review this git diff for:
1. Logic bugs or off-by-one errors
2. Security issues (injection, unvalidated input, exposed secrets)
3. Missing null/error handling on code paths that exist in this diff
Respond ONLY with a JSON object: {\"severity\": \"none|low|medium|high\", \"findings\": [\"<issue1>\", \"<issue2>\"]}
Severity 'high' = commit should be blocked. 'none' = looks clean.
" 2>/dev/null)
SEVERITY=$(echo "$REVIEW" | python3 -c "import sys,json; print(json.load(sys.stdin)['severity'])" 2>/dev/null || echo "none")
FINDINGS=$(echo "$REVIEW" | python3 -c "import sys,json; [print(' •', f) for f in json.load(sys.stdin)['findings']]" 2>/dev/null || true)
if [[ "$SEVERITY" == "high" ]]; then
echo "🚫 Claude pre-commit review: HIGH severity — commit blocked"
echo "$FINDINGS"
exit 1
elif [[ "$SEVERITY" != "none" ]]; then
echo "⚠️ Claude pre-commit review ($SEVERITY):"
echo "$FINDINGS"
fi
How it works
-p(print mode): Claude runs non-interactively, reads from stdin, writes to stdout, exits — perfect for shell pipelines.- Structured JSON output: The prompt constrains Claude to a JSON schema so
python3 -ccan parse severity and findings without fragile regex. exit 1on high severity: Git aborts the commit. Fix and re-stage to proceed.- Non-zero but non-high findings: Printed as warnings, commit proceeds — useful for "I saw this but it's intentional" situations.
Bypass when intentional
git commit --no-verify # bypass all hooks for this commit
SKIP_CLAUDE_REVIEW=1 git commit # add a [[ -n "$SKIP_CLAUDE_REVIEW" ]] && exit 0 guard
Shareable via git template
To apply this to every new repo on your machine:
mkdir -p ~/.git-template/hooks
cp .git/hooks/pre-commit ~/.git-template/hooks/
git config --global init.templateDir ~/.git-template
# future: git init → hook installed automatically
The hook is fastest on small diffs. For large staged sets, add head -c 8000 after git diff --staged to cap the input to Claude.
Sources: Claude Code headless mode (-p flag) — code.claude.com · Git hooks documentation — git-scm.com · Claude Code common workflows — code.claude.com