πŸ“ Tutorials
Β· 5 min read

Build an AI Commit Message Generator β€” Git Hook Tutorial


Writing good commit messages is one of those things every developer agrees matters β€” and every developer occasionally skips. What if your git workflow could handle it for you? In this tutorial, you’ll build a prepare-commit-msg git hook that reads your staged diff, sends it to a local Ollama model, and pre-fills your editor with a conventional commit message. No API keys, no cloud calls, everything runs on your machine.

If you’re new to Ollama, start with our complete Ollama guide to get set up.

How It Works

Git supports hooks β€” scripts that run at specific points in the commit workflow. The prepare-commit-msg hook fires after git creates the default commit message file but before your editor opens. That’s the perfect place to inject an AI-generated message.

The flow:

  1. You run git commit
  2. Our hook grabs the staged diff via git diff --cached
  3. The diff is sent to Ollama with a prompt requesting conventional commit format
  4. Ollama’s response is written into the commit message file
  5. Your editor opens with the message pre-filled β€” edit or accept as-is

The Conventional Commit Format

Our generator follows the Conventional Commits spec:

type(scope): description

optional body

Common types: feat, fix, docs, style, refactor, test, chore, build, ci, perf. The scope is optional and describes the area of the codebase affected.

The Python Script

Create a file called ai_commit_msg.py β€” this is the core logic that the git hook will call:

#!/usr/bin/env python3
"""AI commit message generator using Ollama."""

import subprocess
import sys
import json
import urllib.request

OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL = "qwen2.5-coder:7b"

PROMPT_TEMPLATE = """Write a git commit message for the following diff using the Conventional Commits format.

Rules:
- First line: type(scope): description (max 72 chars)
- Types: feat, fix, docs, style, refactor, test, chore, build, ci, perf
- Scope is optional but preferred
- Add a blank line then a brief body if the change is non-trivial
- Do NOT wrap the message in a code block
- Output ONLY the commit message, nothing else

Diff:
{diff}"""


def get_staged_diff():
    result = subprocess.run(
        ["git", "diff", "--cached", "--diff-algorithm=minimal"],
        capture_output=True, text=True
    )
    return result.stdout.strip()


def generate_message(diff):
    # Truncate very large diffs to stay within context limits
    if len(diff) > 8000:
        diff = diff[:8000] + "\n... (diff truncated)"

    payload = json.dumps({
        "model": MODEL,
        "prompt": PROMPT_TEMPLATE.format(diff=diff),
        "stream": False,
        "options": {"temperature": 0.3, "num_predict": 200}
    }).encode()

    req = urllib.request.Request(
        OLLAMA_URL,
        data=payload,
        headers={"Content-Type": "application/json"}
    )

    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read())["response"].strip()


def main():
    if len(sys.argv) < 2:
        print("Usage: ai_commit_msg.py <commit-msg-file>", file=sys.stderr)
        sys.exit(1)

    commit_msg_file = sys.argv[1]

    # Skip if this is a merge, squash, or amend
    if len(sys.argv) > 2 and sys.argv[2] in ("merge", "squash", "commit"):
        sys.exit(0)

    diff = get_staged_diff()
    if not diff:
        sys.exit(0)

    try:
        message = generate_message(diff)
    except Exception as e:
        print(f"AI commit msg failed ({e}), falling back to default", file=sys.stderr)
        sys.exit(0)

    # Read existing content (contains commented-out status info)
    with open(commit_msg_file, "r") as f:
        existing = f.read()

    with open(commit_msg_file, "w") as f:
        f.write(message + "\n" + existing)


if __name__ == "__main__":
    main()

No external dependencies β€” just Python’s standard library and a running Ollama instance.

Setting Up the Git Hook

You have two options: per-repo or global.

Per-Repo Setup

Copy the script into your repo and create the hook:

# Copy the script somewhere accessible
cp ai_commit_msg.py .git/hooks/

# Create the hook
cat > .git/hooks/prepare-commit-msg << 'EOF'
#!/bin/sh
python3 "$(dirname "$0")/ai_commit_msg.py" "$1" "$2"
EOF

chmod +x .git/hooks/prepare-commit-msg

Global Setup (All Repos)

This is the better option if you want AI messages everywhere:

# Create a global hooks directory
mkdir -p ~/.git-hooks
cp ai_commit_msg.py ~/.git-hooks/

# Create the global hook
cat > ~/.git-hooks/prepare-commit-msg << 'EOF'
#!/bin/sh
python3 ~/.git-hooks/ai_commit_msg.py "$1" "$2"
EOF

chmod +x ~/.git-hooks/prepare-commit-msg

# Tell git to use it
git config --global core.hooksPath ~/.git-hooks

Now every git commit across all your repos will trigger the AI generator.

Try It Out

Stage some changes and commit:

echo "# My Project" > README.md
git add README.md
git commit

Your editor opens with something like:

docs: add project README

Initialize repository with a basic README file.
# Please enter the commit message for your changes...

Edit if you want, save, done. If Ollama isn’t running or anything fails, the hook silently falls back to the normal empty message β€” it never blocks your workflow.

Choosing a Model

The model you pick matters. You want something fast enough to not slow down commits but smart enough to parse diffs. Check our best Ollama models for coding for a full breakdown. Quick recommendations:

ModelSizeSpeedQuality
qwen2.5-coder:3b2 GB~1sGood for simple changes
qwen2.5-coder:7b4.5 GB~2-3sBest balance (default)
codellama:13b7 GB~4-5sHandles complex refactors well

Switch models by editing the MODEL variable in the script, or set it via environment variable by replacing the constant with:

MODEL = os.environ.get("AI_COMMIT_MODEL", "qwen2.5-coder:7b")

Pull your chosen model first:

ollama pull qwen2.5-coder:7b

Tips and Gotchas

  • Large diffs: The script truncates diffs over 8,000 characters. For massive changes, consider breaking commits into smaller pieces β€” that’s better practice anyway. Our git stash cheat sheet can help manage work-in-progress when splitting changes.
  • Skipping the hook: Use git commit --no-verify or git commit -m "your message" (the hook only fires when the editor would open with no pre-set message via -m).
  • Timeout: The script uses a 30-second timeout. If your model is slow on first load, increase it.
  • Temperature: Set to 0.3 for consistent, predictable messages. Bump to 0.5-0.7 if you want more varied output.

Pair It With AI Code Review

This hook handles the writing side. For the review side, check out our guide on local AI code review with Ollama β€” you can build a pre-push hook that reviews your changes before they hit the remote.

What You Built

A zero-dependency Python script that plugs into git’s hook system and generates conventional commit messages using a local LLM. No API keys, no subscriptions, no data leaving your machine. The hook is non-blocking β€” if anything fails, you just get the normal commit flow.

The entire setup takes under five minutes and saves you from writing β€œfix stuff” at 11 PM ever again.

πŸ“˜