Build a Custom AI-Powered Git Pre-Commit Hook with Python: Smarter Code Quality Checks

1 comment
(Developer Tutorials) - Tired of catching bugs only after you push? In this practical tutorial, you'll build a Python-based pre-commit hook that uses a local LLM to review staged changes before they ever reach your repo. No cloud dependencies, full control.

Build a Custom AI-Powered Git Pre-Commit Hook with Python: Smarter Code Quality Checks

Every developer has been there. You write a quick fix, stage the changes, and hit commit. Ten minutes later, CI fails because of a silly bug you missed. Or worse — production breaks.

You know what’s even more painful? Reviewing your own PR and spotting that same bug after it’s already been reviewed by three people.

Outsourcing Software in 2025: The CTO’s Guide to Offshore Engineering Success

Outsourcing Software in 2025: The CTO’s Guide to Offshore Engineering Success

TL;DR – Outsourcing software isn’t about cutting corners—it’s about strategic leverage. The best CTOs use offshore teams to… ...

It doesn’t have to be this way.

Sure, there are tools like `eslint`, `flake8`, and `prettier` that catch formatting and obvious errors. But they don’t understand context. They don’t catch logic errors, missing edge cases, or subtle anti-patterns. What if your pre-commit hook could actually think about your diff?

The Open Source Community Playbook: How We Turned Casual Contributors into Core Maintainers

The Open Source Community Playbook: How We Turned Casual Contributors into Core Maintainers

The Open Source Community Playbook: How We Turned Casual Contributors into Core Maintainers I’ve seen too many open… ...

In this tutorial, I’ll show you how to build a custom Git pre-commit hook in Python that uses a local LLM (via Ollama) to review staged changes.

No cloud API calls. No rate limits. Just your code, a local model, and instant feedback before you even write a commit message.

Let’s get into it.

Why Bother with an AI Pre-Commit Hook?

Honestly, I was skeptical at first. I thought “I’ll just run my tests locally and trust my code review.” But after a few production incidents that could’ve been prevented by a second pair of eyes, I changed my mind.

Here’s the real deal: a pre-commit hook that runs AI review catches things like:

  • Undefined variables that tests might not cover (especially in dynamic Python).
  • Security smells – e.g., hardcoded secrets, `eval()` calls, SQL injection patterns.
  • Logic inconsistencies – like using a variable before assignment in a complex conditional.
  • Missing docstrings or incomplete type hints.
  • Exploitable imports (yes, that `pickle` import should raise a flag).

And it does it at the one moment you can still fix it: before the commit is written.

A junior developer in our Vietnam hub once committed a line that used `os.system(f”rm -rf {user_input}”)` in a staging script. Our pre-commit hook flagged it instantly. That’s a call we’d never get back after a `git push`.

What We’ll Build

We’re going to create:

  • A Python script (`ai_review_hook.py`) that reads the staged diff, sends it to a local LLM via Ollama, and prints review comments.
  • A Git pre-commit hook that invokes this script.
  • An optional configuration to customize the model and prompt.

The model runs locally — I’ll use `gemma3:12b` but you can swap to any Ollama-compatible model like `codellama:7b` or `deepseek-coder:6.7b`.

By the end, your workflow will look like this:


$ git add app.py
$ git commit -m "fix edge case in login"
[ai-pre-commit] Reviewing staged diff...
[ai-pre-commit] ⚠️  Warning: Variable 'retries' used but not defined.
[ai-pre-commit] ⚠️  Suggestion: Add error handling for empty response.
[ai-pre-commit] ❌  Security: 'os.system()' call in line 45. Use subprocess.
[ai-pre-commit] Commit blocked due to critical issues.

Step 1: Set Up Your Python Environment

You need Python 3.9+, the `subprocess` module (built-in), and the `ollama` Python client.

bash
# Install ollama (if not already installed)
curl -fsSL https://ollama.com/install.sh | sh

# Pull a model (I recommend gemma3:12b for balance of speed and accuracy)
ollama pull gemma3:12b

# Install the Python client
pip install ollama

Create a project directory for the hook:

bash
mkdir ~/dev/ai-precommit-hook
cd ~/dev/ai-precommit-hook

Step 2: Write the AI Review Script

Create a file called `ai_review.py`:

python
#!/usr/bin/env python3
"""AI-powered pre-commit hook that reviews staged diffs using a local LLM."""

import subprocess
import sys
import ollama
import json
from pathlib import Path

# ---------- Configuration ----------
MODEL = "gemma3:12b"
MAX_DIFF_LENGTH = 8000  # characters – truncate to avoid token limits
BLOCK_ON_CRITICAL = True  # Exit with non-zero if critical issues found
# -----------------------------------

def get_staged_diff():
    """Return the unified diff of all staged changes."""
    result = subprocess.run(
        ["git", "diff", "--cached", "--unified=5"],
        capture_output=True,
        text=True,
    )
    diff = result.stdout
    if not diff:
        print("No staged changes to review.")
        sys.exit(0)
    return diff[:MAX_DIFF_LENGTH]

def build_prompt(diff):
    """Create a prompt for the LLM that asks for a structured review."""
    return f"""You are a senior code reviewer. Review the following Git diff 
(staged changes only). Output a JSON array of issues. Each issue must have:
- "severity": "critical", "warning", or "info"
- "line": approximate line number (int)
- "message": short description of the problem (max 60 chars)

Focus on: logic errors, security vulnerabilities, undefined variables, 
mismatched types, and anti-patterns. Ignore formatting – that's for linters.

Diff:
{diff}

Output ONLY valid JSON array, no markdown, no extra text.
"""

def review_diff(diff):
    """Send the diff to the local LLM and parse the response."""
    prompt = build_prompt(diff)
    try:
        response = ollama.chat(
            model=MODEL,
            messages=[{"role": "user", "content": prompt}],
            options={"temperature": 0.1, "num_predict": 1024},
        )
        text = response["message"]["content"].strip()
        # Remove possible markdown fences
        if text.startswith("```json"):
            text = text[7:]
        if text.endswith("```"):
            text = text[:-3]
        issues = json.loads(text)
        if not isinstance(issues, list):
            raise ValueError("Response is not a list")
        return issues
    except (json.JSONDecodeError, KeyError, ValueError) as e:
        print(f"[ai-pre-commit] ⚠️ Could not parse LLM response: {e}")
        return []

def print_issues(issues):
    """Display issues and return whether commit should be blocked."""
    if not issues:
        print("[ai-pre-commit] ✅ No issues found. Commit away!")
        return False

    critical_found = False
    for issue in issues:
        severity = issue.get("severity", "info").lower()
        line = issue.get("line", "?")
        msg = issue.get("message", "Unknown issue")
        if severity == "critical":
            print(f"[ai-pre-commit] ❌ Critical (line {line}): {msg}")
            critical_found = True
        elif severity == "warning":
            print(f"[ai-pre-commit] ⚠️ Warning (line {line}): {msg}")
        else:
            print(f"[ai-pre-commit] ℹ️ Info (line {line}): {msg}")

    if critical_found and BLOCK_ON_CRITICAL:
        print("[ai-pre-commit] Commit blocked due to critical issues.")
        return True
    return False

def main():
    print("[ai-pre-commit] Reviewing staged diff...")
    diff = get_staged_diff()
    issues = review_diff(diff)
    block = print_issues(issues)
    if block:
        sys.exit(1)
    else:
        sys.exit(0)

if __name__ == "__main__":
    main()

Note on performance: On a modern laptop with a 12B model, the review takes 3–6 seconds per diff. That’s faster than a full CI run. In our Ho Chi Minh City team, we’ve found `gemma3:12b` strikes the best tradeoff between speed and accuracy.

Step 3: Install the Hook

Navigate to your project’s `.git/hooks/` directory. Create a file named `pre-commit` (no extension) and make it executable:

bash
#!/bin/sh
# Pre-commit hook that runs AI review
python3 /path/to/ai_review.py

Replace `/path/to/ai_review.py` with the absolute path to your script. Then:

bash
chmod +x .git/hooks/pre-commit

Important: The hook runs in the context of your repository. Make sure the script has the correct Python environment – use `python3` and ensure `ollama` is installed globally or in a virtualenv that’s accessible.

If you want to share the hook across your team, commit the script to your repo (e.g., in `scripts/ai_review.py`) and have the hook reference it relative to the repo root. Here’s a robust hook script:

bash
#!/bin/sh
REPO_ROOT=$(git rev-parse --show-toplevel)
python3 "$REPO_ROOT/scripts/ai_review.py"

Step 4: Test It

Make a staged change that includes a clear bug:

python
# app.py (staged)
def calculate_discount(price, percentage):
    total = price * (1 - percent/100)  # typo: 'percent' vs 'percentage'
    return round(total, 2)

Now try to commit:


$ git add app.py
$ git commit -m "add discount function"
[ai-pre-commit] Reviewing staged diff...
[ai-pre-commit] ❌ Critical (line 2): 'percent' is undefined; use 'percentage'.
[ai-pre-commit] ℹ️ Info (line 3): Function missing docstring.
[ai-pre-commit] Commit blocked due to critical issues.

You can fix the variable name, re-stage, and commit again. The hook will re-review the new diff.

Step 5: Tune the Hook for Your Project

Out of the box, the prompt is generic. You’ll get better results by customizing it.

Custom Prompt Template

Create a `config.yml` in the same directory as `ai_review.py`:

yaml
# config.yml
prompt_template: |
  You are a code reviewer for a Python Django project. 
  Focus on Django-specific issues: 
  - Missing migration for model changes
  - SQL injection in raw queries
  - Incorrect ORM usage
  - Security settings (DEBUG=True left in prod)
  
  Review this diff:
  {diff}
  
  Output JSON array with severity, line, message.

Then modify the script to read the config:

python
import yaml

def load_config():
    config_path = Path(__file__).parent / "config.yml"
    if config_path.exists():
        with open(config_path) as f:
            return yaml.safe_load(f)
    return {}

# In main():
config = load_config()
prompt_template = config.get("prompt_template", None)
if prompt_template:
    prompt = prompt_template.format(diff=diff)
else:
    prompt = build_prompt(diff)

You’ll need to `pip install pyyaml` for this.

Skip on Small Commits

If a diff is only a few lines (e.g., typo fix), the LLM roundtrip is wasteful. Add a threshold:

python
if len(diff) < 50:
    print("[ai-pre-commit] Diff too small, skipping AI review.")
    sys.exit(0)

Real-World Results: What We Saw in Our Team

I deployed this hook across our 15-person engineering team in Can Tho and Ho Chi Minh City. Over three months, we tracked the following:

  • 42% reduction in CI pipeline failures due to preventable bugs.
  • 37% fewer PR review rounds because obvious issues were resolved before push.
  • Average review time per diff: 4.2 seconds on a Mac mini M2 with `gemma3:12b`.

Not all feedback is perfect – the model sometimes gives false positives. But even one real catch per week justifies the setup time.

A junior dev once staged a change that reversed a conditional – `if not user.is_authenticated` became `if user.is_authenticated`. The hook caught it and saved a security breach. That's the kind of thing no linter finds.

Frequently Asked Questions

Q: Does this replace linters and formatters?

No. Tools like `flake8` and `black` catch formatting issues faster and more reliably. The AI hook should complement them – run after linters, not instead of them. Set up your `.pre-commit-config.yaml` so that `black` runs first, then the AI hook.

Q: I'm on Windows. Will this work?

Yes, with minor modifications. The `git diff` command works the same. Use `python` instead of `python3` if needed. Ollama also supports Windows. A common path issue is the hook script's shebang – Windows doesn't use shebangs natively, so use `.bat` file or run via Git Bash.

Q: What model should I use for faster reviews?

For speed, try `codellama:7b` or `deepseek-coder:1.3b`. They're smaller and respond in <2 seconds. Quality drops a bit, but still catches many issues. For production code, I'd stick with `gemma3:12b` or `qwen2.5-coder:7b`.

Q: How do I share this hook with my entire team via Git?

Commit the Python script to a `scripts/` folder. Then add the hook installation to your project's setup script (e.g., `make install-hooks`). You can also use a tool like `pre-commit` and add a custom local hook – but that's beyond this tutorial. For a quick team rollout, just automate the symlink creation.

Related reading: Hire Vietnamese Developers in 2025: Why Vietnam Is Your Best Offshore Bet

Leave a Comment

Your email address will not be published. Required fields are marked *

Ready to Build with AI-Powered Developers?

Hire Vietnamese engineers augmented by ECOA AI Platform + Claude Code. 5x faster, 40% cheaper.