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-commithook 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(orllama3.1:8bfor lighter hardware) - A git repository to protect
Verify Ollama is running:
curl http://localhost:11434/api/tags | python3 -m json.tool
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:
{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()
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
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"
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.
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"
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
For a Solana/Rust shop:
Also check for:
- Missing signer validation in Anchor programs
- Unchecked arithmetic that could overflow
- Missing account ownership checks
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]
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
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:
- GitHub Action: Run the reviewer on every PR via a self-hosted runner with Ollama installed
- Slack/Telegram alerts: Post review summaries to a team channel (we use Telegram — see our AI agent memory post)
- SQLite history: Log every review result so you can track which issue types recur most often
-
Language-specific prompts: Auto-detect
.py,.rs,.tsand 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"
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)