Generating Commit Messages from a Diff with AI
A prepare-commit-msg hook and a Python script that draft a Conventional Commits message from the staged diff. You edit it in your editor as usual.
5 min · · development · автоматизация
Generating commit messages from a diff is one of the simplest useful automations for developers: the model reads your staged changes and drafts a Conventional Commits message, and you edit it in your usual editor. Below are a Python script, a git hook, handling of large diffs, and an honest cost estimate.
What you get
git commitwithout-mopens the editor with a draft already on the first line, such asfix(auth): handle expired refresh token.git commit -m "...", merges, and--amendare left alone by the hook.- If the API is unreachable or the answer does not look like Conventional Commits, the commit proceeds as usual with the empty template.
The generator suggests; the commit author still decides.
The generator script
Put the file in the repository as scripts/generate_commit_msg.py. You need pip install openai and a fetch_... key in OPENAI_API_KEY.
#!/usr/bin/env python3
import os
import re
import subprocess
import sys
from openai import OpenAI
client = OpenAI(
base_url="https://api.zukko.pro/v1",
api_key=os.environ["OPENAI_API_KEY"],
)
MODEL = os.environ.get("COMMIT_MSG_MODEL", "glm-5.3-flash")
MAX_DIFF_TOKENS = 3000Why it is built this way:
- The diff comes through stdin, not as an argument: a large diff passed as an argument hits the command-line length limit.
--statin the prompt gives the model the full list of files even when the diff itself is trimmed.- The first line is validated with a regex. If the model replies with reasoning instead of a message, the script exits with an error and the hook simply inserts nothing.
- The model comes from
COMMIT_MSG_MODEL, so you can compare models without editing the code.
The prepare-commit-msg hook
According to the git documentation, prepare-commit-msg runs after the default message is prepared and before the editor starts. The second argument is the message source: message (with -m or -F), template, merge, squash, or commit (with -c, -C, --amend). For a plain git commit it is empty, and that is the only case where we generate.
Note that the message file is not empty at this point: it already holds git's commented template. So a "file is empty" check does not work; the draft has to be prepended to the file.
File .githooks/prepare-commit-msg:
#!/bin/sh
COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="$2"
[ -n "$COMMIT_SOURCE" ] && exit 0
[ -n "$SKIP_AI_MSG" ] && exit 0
MESSAGE=$(git diff --cached | python3 scripts/generate_commit_msg.py) || exit 0
[ -z "$MESSAGE" ] && exit 0
{ printf '%s\n' "$MESSAGE"; cat "$COMMIT_MSG_FILE"; } > "$COMMIT_MSG_FILE.tmp"
mv "$COMMIT_MSG_FILE.tmp" "$COMMIT_MSG_FILE"To enable it for the whole team (.git/hooks is not committed, .githooks is):
chmod +x .githooks/prepare-commit-msg
git config core.hooksPath .githooksGit runs hooks from the root of the working tree, so the relative path scripts/generate_commit_msg.py works from any subfolder. The hook always exits with 0: a generation failure must not block the commit.
Running it manually without the hook
If you do not want a hook, generate a message and open it in the editor before committing:
git diff --cached | python3 scripts/generate_commit_msg.py > /tmp/ai_msg && git commit -e -F /tmp/ai_msg-e opens the editor and -F reads the text from a file. Since the source here is message, the hook will not generate a second time.
Large diffs
An updated lock file or generated code can easily produce a diff with tens of thousands of lines. Sending all of it makes no sense: it costs more, and a commit message describes the intent, not every line. smart_truncate keeps file and hunk headers (diff --git, @@) plus as much content as fits into a budget of about 3,000 tokens, and cuts the rest with a marker.
Two more tricks:
- exclude noisy files right in git:
git diff --cached -- . ':(exclude)package-lock.json'; - for huge commits it is more honest to split them into several logical ones; the model cannot help with that.
What it costs
Assumptions: the prompt with rules and --stat is about 300 tokens, an average trimmed diff is about 1,700, so 2,000 input tokens in total; the answer is about 60 tokens. Per 1,000 commits that is 2,000,000 input and 60,000 output tokens.
| Model | Formula per 1,000 commits | Total |
|---|---|---|
glm-5.3-flash ($0.04 / $0.04) | (2,000,000 × 0.04 + 60,000 × 0.04) / 1,000,000 | ≈ $0.08 |
claude-sonnet-5 ($0.3 / $0.3) | (2,000,000 × 0.3 + 60,000 × 0.3) / 1,000,000 | ≈ $0.62 |
Worst case, the diff hits the 3,000-token cap: about 3,300 input and 150 output tokens, which is (3,300 + 150) × 0.04 / 1,000,000 ≈ $0.00014 per commit on glm-5.3-flash.
A team of 10 people making 5 commits a day each produces 50 commits a day, about 1,100 a month (22 working days): roughly $0.09 a month on glm-5.3-flash or $0.68 on claude-sonnet-5. Which model writes messages you edit less often is for you to check: run the script with COMMIT_MSG_MODEL over your last 20–30 commits and compare with what people wrote. The current model list is at GET https://api.zukko.pro/v1/models or on the "Models" page in the dashboard.
A separate key for the hook is handy: in the dashboard you can restrict it to a list of allowed models and a daily dollar limit; see key limits. Create a key, and check the quick start for general setup.
FAQ
The hook does not fire. What should I check?
git config core.hooksPathpoints to the hook folder and the file is executable (chmod +x).- You commit without
-m: with-mthe source ismessageand the hook exits on purpose. OPENAI_API_KEYis visible in the environment where git runs. GUI clients and IDEs often do not read your.zshrcor.bashrc.- Run the script by hand:
git diff --cached | python3 scripts/generate_commit_msg.py; the error will show up in stderr.
How do I turn generation off temporarily?
SKIP_AI_MSG=1 git commit: the hook checks this variable. --no-verify will not help here: per the git documentation it does not suppress prepare-commit-msg, only pre-commit and commit-msg.
The model mixes up feat and fix. How do I fix that?
Add your team's rules to PROMPT: "feat is new user-facing behavior; fix is a bug fix; refactor changes no behavior; tests only is test; docs only is docs". Two or three examples from your own repository history work better than long explanations.
How do I hook this up with Husky?
In Husky v9 a hook is a plain shell script in .husky, without the husky.sh sourcing line from older versions. Create .husky/prepare-commit-msg with the same content as the example above; Husky sets core.hooksPath itself.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Translating subtitles with a neural network: SRT without broken timecodesThe main risk when translating SRT with a neural network is a broken file structure. A scheme where timecodes never reach the model, and a Python script with validation.
- Editing texts with a neural network: proofreading, style and cuttingDifferent kinds of editing need different prompts. Wording for proofreading, style and cutting, how to get edits with explanations and how not to lose the author's voice.
- A neural network for writing scripts: Bash, PowerShell and PythonA model writes scripts for routine fast, but running them blindly is risky. How to state the task, demand a safe dry-run mode and check the result before launch.
- Dify in Docker: installation, first-run setup and connecting modelsInstall Dify with Docker Compose, create the admin account and connect Claude, GPT and DeepSeek models on one key. Plus what to do when containers will not start.