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-msghook 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
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:
- Read staged changes with
git diff --cached. - Send the diff to Ollama with a strict prompt.
- 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."