DEV Community

SatStack
SatStack

Posted on

Build a Free AI Code Review Bot in Python with Ollama and Git Hooks

Build a Free AI Code Review Bot in Python with Ollama and Git Hooks

Published: 2026-02-22

Tags: python, ollama, ai, devtools, git

Series: Local AI Stack (Part 6)

Target: dev.to @satstack

SEO Keywords: AI code review Python, Ollama code review, local LLM git hooks, free AI code reviewer, Python git pre-commit hook AI


Every team wants AI-assisted code review. Most reach for expensive SaaS — Copilot, CodeRabbit, PR-Agent — that bills by the seat or API token. Here's the truth: you can run a fully local, zero-cost AI code reviewer in under an hour using Python and Ollama.

This is Part 6 of our Local AI Stack series. We've already covered RAG with Python and a local LLM and giving your AI agent long-term memory. Today we wire Ollama directly into your git workflow.

By the end of this post you'll have:

  • A Python script that reviews staged git diffs with a local LLM
  • A git pre-commit hook that blocks bad commits automatically
  • A standalone CLI tool for on-demand review of any file or PR diff
  • Configurable severity thresholds so the bot only blocks real problems

Why Local Code Review?

Before we build, let's be clear on the tradeoff table:

Feature Cloud AI (Copilot, etc.) Local (Ollama)
Cost $10–$39/seat/month $0
Privacy Code leaves your machine Air-gapped
Latency ~2–5s (network) 1–8s (GPU/CPU)
Customization Prompt tweaks only Full model + prompt control
Offline use

For solo devs, small teams, or anyone working on proprietary code, local wins on every dimension except raw model quality. And with qwen2.5:14b or llama3.1:8b, the quality is close enough to be genuinely useful.


Prerequisites

  • Python 3.10+
  • Ollama installed and running (ollama serve)
  • A model pulled: ollama pull qwen2.5:14b (or llama3.1:8b for lighter hardware)
  • A git repository to protect

Verify Ollama is running:

curl http://localhost:11434/api/tags | python3 -m json.tool
Enter fullscreen mode Exit fullscreen mode

You should see your available models listed.


Step 1 — The Core Reviewer Script

Create ~/tools/ai_code_review.py:

#!/usr/bin/env python3
"""
ai_code_review.py — Local AI code reviewer using Ollama.
Usage:
  python ai_code_review.py --staged          # review git staged diff
  python ai_code_review.py --file path/to/file.py
  python ai_code_review.py --diff patch.diff
"""

import argparse
import json
import subprocess
import sys
import urllib.request
from dataclasses import dataclass
from typing import Optional

OLLAMA_URL = "http://localhost:11434/api/generate"
DEFAULT_MODEL = "qwen2.5:14b"
MAX_DIFF_CHARS = 8000  # Keep prompt manageable

REVIEW_PROMPT = """You are a senior software engineer performing a code review.
Analyze the following code diff and provide feedback.

Focus on:
1. **Bugs** — logic errors, off-by-one, null pointer risks, race conditions
2. **Security** — hardcoded secrets, SQL injection, input validation gaps
3. **Performance** — obvious bottlenecks, N+1 queries, memory leaks
4. **Style** — naming, complexity, missing docstrings (flag but don't block)

For each issue found, output exactly this format:
SEVERITY: [CRITICAL|WARNING|INFO]
LINE: [line number or N/A]
ISSUE: [one-sentence description]
FIX: [one-sentence suggestion]

If no issues found, output: NO_ISSUES

Diff to review:
Enter fullscreen mode Exit fullscreen mode

{diff}

"""

@dataclass
class ReviewIssue:
    severity: str
    line: str
    issue: str
    fix: str

def get_staged_diff() -> str:
    """Get the current staged git diff."""
    result = subprocess.run(
        ["git", "diff", "--cached", "--unified=3"],
        capture_output=True, text=True
    )
    if result.returncode != 0:
        print(f"[ai-review] git diff failed: {result.stderr}", file=sys.stderr)
        sys.exit(1)
    return result.stdout

def get_file_diff(path: str) -> str:
    """Read a file and format it as a pseudo-diff."""
    with open(path) as f:
        content = f.read()
    lines = [f"+{line}" for line in content.splitlines()]
    return f"--- /dev/null\n+++ {path}\n" + "\n".join(lines)

def call_ollama(prompt: str, model: str) -> str:
    """Send a prompt to Ollama and return the response text."""
    payload = json.dumps({
        "model": model,
        "prompt": prompt,
        "stream": False,
        "options": {"temperature": 0.1, "top_p": 0.9}
    }).encode()

    req = urllib.request.Request(
        OLLAMA_URL,
        data=payload,
        headers={"Content-Type": "application/json"},
        method="POST"
    )
    with urllib.request.urlopen(req, timeout=120) as resp:
        data = json.loads(resp.read())
    return data.get("response", "")

def parse_issues(response: str) -> list[ReviewIssue]:
    """Parse the structured LLM response into ReviewIssue objects."""
    if "NO_ISSUES" in response:
        return []

    issues = []
    blocks = response.strip().split("\n\n")
    for block in blocks:
        lines = {
            k.strip(): v.strip()
            for line in block.splitlines()
            if ":" in line
            for k, v in [line.split(":", 1)]
        }
        if "SEVERITY" in lines and "ISSUE" in lines:
            issues.append(ReviewIssue(
                severity=lines.get("SEVERITY", "INFO"),
                line=lines.get("LINE", "N/A"),
                issue=lines.get("ISSUE", ""),
                fix=lines.get("FIX", "")
            ))
    return issues

def review(diff: str, model: str, block_on: list[str]) -> bool:
    """
    Run the review. Returns True if commit should be allowed.
    block_on: list of severities that should block (e.g. ["CRITICAL", "WARNING"])
    """
    if not diff.strip():
        print("[ai-review] No changes to review.")
        return True

    # Truncate if diff is huge
    if len(diff) > MAX_DIFF_CHARS:
        print(f"[ai-review] Diff truncated to {MAX_DIFF_CHARS} chars (was {len(diff)})")
        diff = diff[:MAX_DIFF_CHARS] + "\n... [truncated]"

    print(f"[ai-review] Reviewing with {model}...", flush=True)
    prompt = REVIEW_PROMPT.format(diff=diff)

    try:
        response = call_ollama(prompt, model)
    except Exception as e:
        print(f"[ai-review] Ollama unreachable: {e} — skipping review", file=sys.stderr)
        return True  # Fail open so Ollama outage doesn't break workflow

    issues = parse_issues(response)

    if not issues:
        print("[ai-review] ✅ No issues found.")
        return True

    blocked = False
    for issue in issues:
        icon = {"CRITICAL": "🔴", "WARNING": "🟡", "INFO": "🔵"}.get(issue.severity, "⚪")
        print(f"\n{icon} [{issue.severity}] Line {issue.line}")
        print(f"   Issue: {issue.issue}")
        if issue.fix:
            print(f"   Fix:   {issue.fix}")
        if issue.severity in block_on:
            blocked = True

    if blocked:
        print("\n[ai-review] ❌ Commit BLOCKED — fix CRITICAL/WARNING issues above.")
    else:
        print("\n[ai-review] ✅ Commit allowed (only INFO-level issues).")

    return not blocked

def main():
    parser = argparse.ArgumentParser(description="AI code reviewer using Ollama")
    source = parser.add_mutually_exclusive_group(required=True)
    source.add_argument("--staged", action="store_true", help="Review staged git diff")
    source.add_argument("--file", metavar="PATH", help="Review a specific file")
    source.add_argument("--diff", metavar="PATH", help="Review a .diff/.patch file")
    parser.add_argument("--model", default=DEFAULT_MODEL)
    parser.add_argument("--block-on", default="CRITICAL",
                        help="Comma-separated severities that block commit (default: CRITICAL)")
    args = parser.parse_args()

    block_on = [s.strip().upper() for s in args.block_on.split(",")]

    if args.staged:
        diff = get_staged_diff()
    elif args.file:
        diff = get_file_diff(args.file)
    elif args.diff:
        with open(args.diff) as f:
            diff = f.read()

    allowed = review(diff, args.model, block_on)
    sys.exit(0 if allowed else 1)

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

This is pure stdlib — no pip install required. Ollama is the only dependency.


Step 2 — Wire It as a Git Pre-Commit Hook

This is where the magic happens. Every time you git commit, the bot reviews your staged changes first.

# Install the hook into your repo
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
python3 ~/tools/ai_code_review.py --staged --block-on "CRITICAL"
EOF
chmod +x .git/hooks/pre-commit
Enter fullscreen mode Exit fullscreen mode

Now test it with a deliberately bad commit:

# Create a file with a hardcoded secret (classic mistake)
echo 'API_KEY = "sk-1234abcd"' > bad_code.py
git add bad_code.py
git commit -m "add config"
Enter fullscreen mode Exit fullscreen mode

Expected output:

[ai-review] Reviewing with qwen2.5:14b...

🔴 [CRITICAL] Line 1
   Issue: Hardcoded API key exposed in source code — will be committed to version history.
   Fix:   Move to environment variable: os.environ.get('API_KEY')

[ai-review] ❌ Commit BLOCKED — fix CRITICAL/WARNING issues above.
Enter fullscreen mode Exit fullscreen mode

The commit fails. Clean up, fix the issue, recommit. Done.


Step 3 — Standalone CLI for PR Reviews

Sometimes you want to review an entire file or a downloaded PR diff without committing anything. The same script handles it:

# Review any file on-demand
python3 ~/tools/ai_code_review.py --file src/auth.py

# Review a downloaded PR diff
curl -H "Authorization: token $GITHUB_TOKEN" \
  https://api.github.com/repos/owner/repo/pulls/42 \
  -H "Accept: application/vnd.github.v3.diff" \
  > pr42.diff
python3 ~/tools/ai_code_review.py --diff pr42.diff --block-on "CRITICAL,WARNING"
Enter fullscreen mode Exit fullscreen mode

Step 4 — Tuning for Your Team

Choosing the Right Model

Hardware Recommended Model Speed
CPU only (16GB RAM) llama3.1:8b ~15–30s per review
GPU (8GB VRAM) qwen2.5:14b ~3–8s per review
GPU (16GB+ VRAM) qwen2.5:32b or deepseek-coder-v2:16b ~5–12s per review

For code review specifically, deepseek-coder-v2:16b often outperforms general models — it was trained heavily on code and understands language-specific patterns.

Pull it with: ollama pull deepseek-coder-v2:16b

Adjust the Prompt for Your Stack

The default prompt is language-agnostic. For a Python shop, add to REVIEW_PROMPT:

Also check for:
- Missing type hints on public functions
- Mutable default arguments (a classic Python footgun)
- Unhandled exceptions in async code
- Use of bare `except:` clauses
Enter fullscreen mode Exit fullscreen mode

For a Solana/Rust shop:

Also check for:
- Missing signer validation in Anchor programs
- Unchecked arithmetic that could overflow
- Missing account ownership checks
Enter fullscreen mode Exit fullscreen mode

Team Shared Hook via pre-commit Framework

If you're using the pre-commit framework, add this to .pre-commit-config.yaml:

repos:
  - repo: local
    hooks:
      - id: ai-code-review
        name: AI Code Review (Ollama)
        entry: python3 tools/ai_code_review.py --staged --block-on CRITICAL
        language: system
        pass_filenames: false
        stages: [commit]
Enter fullscreen mode Exit fullscreen mode

Step 5 — Fail Open, Not Closed

Notice this line in the script:

except Exception as e:
    print(f"[ai-review] Ollama unreachable: {e} — skipping review", file=sys.stderr)
    return True  # Fail open
Enter fullscreen mode Exit fullscreen mode

This is intentional. If Ollama is down (restart, model swap, memory pressure), the hook allows the commit anyway. You don't want a local AI outage to brick your git workflow.

If you want stricter behavior (production monorepo, etc.), flip it to return False.


Real Results

I ran this on the btc-api-monitor codebase — a Python tool that monitors crypto API health and sends alerts. In one session, the bot caught:

  • 2 CRITICAL: hardcoded fallback URL that would hit a third-party server in production
  • 3 WARNING: missing timeout parameters on requests.get() calls (hanging risk)
  • 6 INFO: missing docstrings on internal helpers

The CRITICAL flags were real bugs I would have shipped. The WARNING flags were also legitimate. The INFO items were style preferences — easy to ignore or fix.

Total review time: ~4 seconds per commit on a GPU-accelerated 14B model.


What's Next

This bot is intentionally simple. Here's where you can take it:

  1. GitHub Action: Run the reviewer on every PR via a self-hosted runner with Ollama installed
  2. Slack/Telegram alerts: Post review summaries to a team channel (we use Telegram — see our AI agent memory post)
  3. SQLite history: Log every review result so you can track which issue types recur most often
  4. Language-specific prompts: Auto-detect .py, .rs, .ts and load the right prompt template

Want the extended version with GitHub Actions, Slack integration, and SQLite review history? Drop a comment and I'll build Part 7.


TL;DR

# 1. Save the script
curl -o ~/tools/ai_code_review.py https://raw.githubusercontent.com/Mint-Claw/...

# 2. Install as git hook
echo '#!/bin/bash
python3 ~/tools/ai_code_review.py --staged --block-on "CRITICAL"' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

# 3. Never ship a hardcoded secret again
git commit -m "hopefully not a disaster"
Enter fullscreen mode Exit fullscreen mode

Zero cost. Zero cloud. Zero excuses for shipping bugs. 🚀


Part of the Local AI Stack series — building real tools with local LLMs, no API costs.

Top comments (0)