How to Build an AI Commit Message Generator with Ollama

Bad commit messages waste time. "fix stuff" or "update" tells you nothing six months later when you are hunting a regression. Good commit messages are specific, scoped, and follow a consistent format — but writing them for every commit is tedious.

This tutorial shows you how to build a local AI commit message generator that reads your staged changes and writes the message for you. It runs entirely on your machine using Ollama, so no code leaves your laptop and no API key is required.

1. What You Will Build

By the end, you will have:

  • A Python script that calls Ollama with your git diff.
  • A Git prepare-commit-msg hook that auto-fills commit messages.
  • A fallback to a blank message if the model is unavailable.

The result looks like this when you run git commit:

$ git commit
[main 8a3f2c1] feat(auth): add password validation to login endpoint
 3 files changed, 42 insertions(+), 8 deletions(-)

The message was generated locally by a small LLM in under two seconds.

2. Prerequisites

You need three things installed:

  • Git 2.45 or newer
  • Python 3.10 or newer
  • Ollama with a model that supports tool-free text generation (Llama 3.1, Mistral, or Qwen 2.5 work well)

Install the Ollama Python client:

pip install ollama

Pull a lightweight model. For commit messages, smaller models are faster and good enough:

ollama pull llama3.1:8b
Tested with: Ollama 0.5.7, Python 3.12.4, Git 2.45.2, llama3.1:8b. A 7B–8B model is sufficient for this task and keeps generation under two seconds on modern CPUs.

3. How the Git Hook Works

Git exposes several hooks. The one we want is prepare-commit-msg, which runs after you type git commit but before the editor opens. Git passes the commit message file path as the first argument, so our hook can overwrite the default message with one generated by the model.

The flow is simple:

  1. Read staged changes with git diff --cached.
  2. Send the diff to Ollama with a strict prompt.
  3. Parse the model output and write it to the commit message file.

We keep the prompt narrow so the model does not ramble. The instructions explicitly request the Conventional Commits format and forbid explanations.

4. The Python Script

Create a file called generate_commit_message.py in your repository root:

#!/usr/bin/env python3
"""Generate a Conventional Commit message from staged changes using Ollama."""

import os
import re
import subprocess
import sys

import ollama

MODEL = os.getenv("OLLAMA_COMMIT_MODEL", "llama3.1:8b")
MAX_DIFF_LINES = 300

PROMPT = """You are a helpful assistant that writes Git commit messages.

Rules:
- Use Conventional Commits format: type(scope): description
- Allowed types: feat, fix, docs, style, refactor, test, chore
- Keep the subject line under 72 characters
- Be specific. Mention the affected component when possible.
- Do not explain your reasoning.
- Output only the commit message subject line. No quotes, no markdown.

Examples:
feat(auth): add OAuth2 login with Google
fix(api): handle missing user_id in request body
docs(readme): update installation instructions
refactor(db): extract user queries into repository class

Here is the git diff of the staged changes:

{diff}
"""


def get_staged_diff() -> str:
    """Return the staged diff, truncated to avoid token limits."""
    result = subprocess.run(
        ["git", "diff", "--cached", "--no-color"],
        capture_output=True,
        text=True,
        check=True,
    )
    lines = result.stdout.splitlines()
    if len(lines) > MAX_DIFF_LINES:
        lines = lines[:MAX_DIFF_LINES]
        lines.append("\n... diff truncated ...")
    return "\n".join(lines)


def clean_message(text: str) -> str:
    """Strip noise from the model output."""
    text = text.strip()
    # Remove surrounding quotes if present
    text = text.strip('"\'')
    # Collapse whitespace
    text = re.sub(r"\s+", " ", text)
    # Keep only the first line
    text = text.split("\n")[0]
    return text


def generate_message(diff: str) -> str | None:
    """Ask Ollama for a commit message."""
    if not diff.strip():
        return None

    try:
        response = ollama.generate(
            model=MODEL,
            prompt=PROMPT.format(diff=diff),
            options={"temperature": 0.2, "num_predict": 80},
        )
    except ollama.ResponseError as e:
        print(f"Ollama error: {e}", file=sys.stderr)
        return None

    return clean_message(response["response"])


def main() -> int:
    commit_msg_file = sys.argv[1] if len(sys.argv) > 1 else None
    if not commit_msg_file:
        print("Usage: generate_commit_message.py <commit-msg-file>", file=sys.stderr)
        return 1

    diff = get_staged_diff()
    message = generate_message(diff)

    if message:
        with open(commit_msg_file, "w", encoding="utf-8") as f:
            f.write(message + "\n")
        print(f"Generated commit message: {message}")
    else:
        print("No message generated. Using default.")

    return 0


if __name__ == "__main__":
    sys.exit(main())

The script deliberately limits the diff length. Sending a 5,000-line refactor to an 8B model is slow and produces worse results. For huge changes, truncate or summarize.

5. Install the Git Hook

Make the script executable and create the hook in your repository:

chmod +x generate_commit_message.py

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

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

Now make some changes, stage them, and commit:

echo "# TODO" >> README.md
git add README.md
git commit

You should see the generated message in your editor. If you use -m, it will be used automatically:

git commit -m ""

6. Per-Repo vs Global Installation

The hook above is local to one repository because Git hooks live in .git/hooks. To use it across all repositories, create a Git template directory:

mkdir -p ~/.config/git/template/hooks
cp .git/hooks/prepare-commit-msg ~/.config/git/template/hooks/

git config --global init.templateDir ~/.config/git/template

New repositories cloned or initialized after this will include the hook. Existing repos need the hook copied manually or re-initialized with git init.

7. Customizing the Prompt

The prompt is the most important part. If your team uses a different commit style, edit PROMPT to match. For example, Jira-style tickets:

PROMPT = """Write a Git commit message.

Rules:
- Format: [PROJECT-123] type: description
- Allowed types: feature, bugfix, hotfix, docs, refactor, test
- Mention the Jira ticket if the branch name contains it.
- Keep the subject under 72 characters.
- Output only the subject line.

Branch: {branch}
Diff:
{diff}
"""

Pass the branch name by adding this to the script:

def get_branch() -> str:
    result = subprocess.run(
        ["git", "branch", "--show-current"],
        capture_output=True, text=True, check=True
    )
    return result.stdout.strip()

8. Common Mistakes

8.1. Using a model that is not pulled

If Ollama has not downloaded the model, the hook fails silently and leaves the default message. Always pull the model first with ollama pull llama3.1:8b.

8.2. Sending the entire diff for large merges

Merge commits or huge refactors can exceed context windows. The MAX_DIFF_LINES guard helps, but for very large changes consider skipping auto-generation:

if len(diff.splitlines()) > 500:
    return None

8.3. Forgetting the hook is installed

Auto-generated messages are suggestions, not laws. You can still edit the message in your editor before saving. If you want a manual override flag, add this to the hook:

if [ -n "$SKIP_AI_COMMIT" ]; then
    exit 0
fi

Then run SKIP_AI_COMMIT=1 git commit to bypass generation.

8.4. Leaking secrets in the diff

Even though Ollama runs locally, the diff may contain API keys or passwords. Add a pre-check that rejects diffs with common secret patterns, or restrict this tool to repositories without sensitive data.

9. When Not to Use AI Commit Messages

AI-generated messages work best for small, focused changes. They struggle with:

  • Cross-file architectural changes that need human explanation
  • Commits that bundle unrelated fixes
  • Squash merges where the summary should describe the whole feature

Use the generator as a starting point, then edit. A good rule of thumb: if the diff is hard for a human to summarize in one line, do not let the model try.

Frequently Asked Questions

Does this work with Windows?

Yes. Use the Python script directly and create .git/hooks/prepare-commit-msg without a file extension. Git for Windows runs it through the configured shell.

Can I use this with a cloud LLM instead of Ollama?

Yes. Replace the ollama.generate() call with any OpenAI-compatible client. The rest of the script stays the same.

How do I uninstall the hook?

Delete .git/hooks/prepare-commit-msg from the repository. For the global template, remove it from ~/.config/git/template/hooks.

Why does the message sometimes include quotes?

Some models wrap output in quotes. The clean_message() function strips them, but if your model is persistent, tighten the prompt: "Output only the raw commit message. Do not add quotes."