Build a Custom AI-Powered Git Pre-Commit Hook with Python: Smarter Code Quality Checks
Let’s be honest. Standard pre-commit hooks are fine for catching trailing whitespace or running `black`. But they’re dumb. They can’t tell you that your function has a subtle off-by-one error, or that your variable naming is misleading.
I got tired of reviewing PRs where the code “passed all checks” but was logically broken. So I built something better.
I Benchmarked 5 AI Coding Tools on a Real Production Bug — Only 1 Survived
I Benchmarked 5 AI Coding Tools on a Real Production Bug — Only 1 Survived Let’s be honest.… ...
Here’s how to create a custom AI-powered Git pre-commit hook in Python that uses an LLM to review your staged changes. It catches the stuff linters miss. And it runs in under 5 seconds per file.
Why Bother with an AI Pre-Commit Hook?
Standard hooks are rule-based. They check formatting, import order, maybe type hints. But they don’t understand *intent*.
The State of Open-Source AI in 2026: From Agents to Code Generation
2026 has been a watershed year for open-source AI. From ECOA AI Platform’s agent orchestration to smaller specialized… ...
An AI-powered hook can:
- Detect logical errors that pass type checks
- Flag misleading variable names (e.g., `user_list` when it’s actually a dict)
- Enforce team-specific conventions that aren’t in any linter config
- Block dangerous patterns like hardcoded secrets or unsafe `eval()` calls
We’ve been using this setup with our team in Ho Chi Minh City for the last 3 months. Our code review cycle dropped by about 40%. Why? Because the dumb stuff gets caught before anyone sees the PR.
The Architecture
Here’s the flow:
- Git triggers the pre-commit hook
- We grab the staged files using `git diff –cached`
- For each changed file, we send the diff + context to an LLM
- The LLM returns issues or an “all clear”
- If issues exist, we block the commit with details
Simple. Effective.
Step 1: The Python Script
Create a file called `ai_pre_commit.py` in your project’s `scripts/` directory:
python
#!/usr/bin/env python3
"""AI-powered pre-commit hook that reviews staged changes."""
import subprocess
import sys
import json
import os
from pathlib import Path
# You can swap this for any LLM API
# We use Claude via Anthropic API, but OpenAI works too
API_KEY = os.environ.get("AI_REVIEW_API_KEY")
API_ENDPOINT = "https://api.anthropic.com/v1/messages"
def get_staged_diff():
"""Get the diff of all staged changes."""
result = subprocess.run(
["git", "diff", "--cached", "--unified=3"],
capture_output=True,
text=True,
check=True
)
return result.stdout
def get_staged_files():
"""Get list of staged Python files."""
result = subprocess.run(
["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
capture_output=True,
text=True,
check=True
)
files = result.stdout.strip().split("\n")
return [f for f in files if f.endswith(".py")]
def review_diff(file_path, diff_content):
"""Send diff to LLM for review."""
prompt = f"""You are a senior code reviewer. Review the following git diff for the file {file_path}.
Focus on:
1. Logical errors or bugs
2. Security vulnerabilities
3. Violations of team conventions (we use snake_case, type hints, and no wildcard imports)
4. Misleading variable names
5. Missing error handling
Respond in JSON format:
{{"issues": [{{"line": , "severity": "error"|"warning", "message": ""}}], "approved": }}
If no issues, return {{"issues": [], "approved": true}}
Diff:
{diff_content}
"""
# This is a simplified request - you'll need proper error handling
import requests
response = requests.post(
API_ENDPOINT,
headers={
"x-api-key": API_KEY,
"content-type": "application/json",
"anthropic-version": "2023-06-01"
},
json={
"model": "claude-sonnet-4-20250514",
"max_tokens": 1000,
"messages": [{"role": "user", "content": prompt}]
},
timeout=30
)
response.raise_for_status()
content = response.json()["content"][0]["text"]
# Extract JSON from response
try:
return json.loads(content)
except json.JSONDecodeError:
return {"issues": [{"line": 0, "severity": "error", "message": "Failed to parse AI review"}], "approved": False}
def main():
files = get_staged_files()
if not files:
sys.exit(0)
print(f"🔍 AI reviewing {len(files)} staged files...")
all_issues = []
has_errors = False
for file_path in files:
# Get diff for just this file
result = subprocess.run(
["git", "diff", "--cached", "--", file_path],
capture_output=True,
text=True,
check=True
)
diff = result.stdout
if not diff.strip():
continue
print(f" Reviewing {file_path}...")
review = review_diff(file_path, diff)
for issue in review.get("issues", []):
all_issues.append({
"file": file_path,
**issue
})
if issue.get("severity") == "error":
has_errors = True
if all_issues:
print("\n❌ AI Review found issues:")
for issue in all_issues:
icon = "🔴" if issue["severity"] == "error" else "🟡"
print(f" {icon} {issue['file']}:{issue.get('line', '?')} - {issue['message']}")
if has_errors:
print("\n⛔ Commit blocked. Fix errors above and try again.")
sys.exit(1)
else:
print("\n⚠️ Warnings found. Commit allowed but review recommended.")
else:
print("✅ AI review passed. No issues found.")
sys.exit(0)
if __name__ == "__main__":
main()
Step 2: The Git Hook
Create `.git/hooks/pre-commit` in your repo:
bash
#!/bin/bash
# AI-powered pre-commit hook
echo "Running AI code review..."
python3 scripts/ai_pre_commit.py
# Exit code from Python script determines if commit proceeds
exit $?
Make it executable:
bash
chmod +x .git/hooks/pre-commit
Step 3: Configuration
Add this to your `pyproject.toml` or create a `.ai-review-config.json`:
json
{
"api_endpoint": "https://api.anthropic.com/v1/messages",
"model": "claude-sonnet-4-20250514",
"max_tokens": 1000,
"timeout_seconds": 30,
"rules": {
"no_hardcoded_secrets": true,
"require_type_hints": true,
"max_function_length": 50,
"forbidden_patterns": ["eval(", "exec(", "pickle.loads"]
},
"file_extensions": [".py", ".js", ".ts", ".go"]
}
Real Results
We rolled this out across 3 projects at ECOA AI. Here’s what we saw in the first month:
| Metric | Before | After |
|---|---|---|
| PRs requiring rework | 34% | 12% |
| Average review time per PR | 45 min | 22 min |
| Bugs caught in review | 8 | 23 |
| False positives from hook | N/A | 3 |
The false positives were annoying. We tuned the prompt to be more conservative on warnings. Now it only flags things with high confidence.
Performance Considerations
You don’t want your hook to take 30 seconds per file. Here’s how we keep it fast:
- Only review changed lines, not the whole file
- Use a fast model like Claude Haiku for simple checks, Sonnet for complex ones
- Cache results for unchanged files (though git already handles this)
- Set a timeout — if the API takes longer than 10 seconds, skip the review
python
# Add this to your review function
import time
start = time.time()
review = review_diff(file_path, diff)
elapsed = time.time() - start
if elapsed > 5:
print(f" ⚠️ Review took {elapsed:.1f}s for {file_path}")
The Catch
This isn’t perfect. Here’s what I’ve learned:
- LLMs hallucinate issues. You’ll get false positives. Tune your prompt aggressively.
- API costs add up. At ~100 reviews/day, we spend about $15/month. Worth it.
- It’s not a replacement for human review. It catches surface-level logic issues, not architectural problems.
But honestly? It’s way better than the alternative — pushing broken code and finding out in CI 10 minutes later.
Making It Team-Friendly
If you’re working with a team (especially a distributed one like ours in Vietnam), you need to make this easy to set up:
bash
# Add to your setup script
pip install requests
cp scripts/ai_pre_commit.py .git/hooks/
chmod +x .git/hooks/pre-commit
echo "AI_REVIEW_API_KEY=your_key_here" >> .env
We include this in our onboarding docs. New devs in Can Tho or Ho Chi Minh City can be up and running in 2 minutes.
Frequently Asked Questions
Q: Does this work with any LLM provider?
A: Yes. The code uses Anthropic’s API, but you can swap it for OpenAI, Google Gemini, or even a local model via Ollama. Just change the endpoint and request format.
Q: Won’t this slow down my commits?
A: It adds 2-5 seconds per file. We review only staged Python files, so most commits take under 10 seconds total. You can also add a `–no-ai` flag to skip the review for urgent fixes.
Q: How do I handle false positives without disabling the hook?
A: Add a `# ai-review: ignore` comment to suppress warnings on specific lines. Parse for this in your script and skip those lines during review.
Q: Can I use this for non-Python files?
A: Absolutely. The script filters by `.py` extension, but you can easily extend it to support JavaScript, TypeScript, Go, or any language. The LLM doesn’t care about the language — it reads the diff.
Related reading: Vietnam Outsourcing: Why Smart Tech Leaders Are Betting on This Southeast Asian Powerhouse