I wrote previously about my rules for writing Git commit messages. I like to follow the 50/72 rule and use the imperative mood in my commit subjects. While the rules are simple, and the benefits of following them are improved readability of project history, I don’t want to spend time enforcing git commit message rules.
Especially in current times when git commits may not be written by humans, but by AI tools. Now I could formalize my rules with instructions:
- Subject ≤ 50 chars, imperative mood, no period.
- No type prefixes — do not use `feat:`, `fix:`, `chore:`, `bug:`, or
any other conventional-commit-style prefix.
- Examples: `Fix flaky ...`, `Remove dead code`, `Move ...`
- Separate subject from body with a blank line; wrap body at 72 chars.
- No AI-generated attribution in commit messages.
These are helpful and I would include them as instructions in an Agent file however, they will not be followed in all circumstances.
Therefore use a git commit-msg hook to enforce the rules. The hook will check the commit message and reject it if it does not follow the rules. If you install the git commit-msg hook in your repositiory, I can guarantee you will see that it frequently rejects commit messages that do not follow the rules.
#!/usr/bin/env bash
# Drop everything from the scissors line down (git commit -v diff),
# then strip comment lines
msg=$(sed '/^# -\+ >8 -\+$/,$d' "$1" | grep -v '^#')
subject=$(printf '%s' "$msg" | head -1)
# Skip autosquash targets and merge commits — not subject to normal formatting
if [[ "$subject" =~ ^(fixup|squash|amend)\!\ ]] || [[ "$subject" == Merge\ * ]]; then
exit 0
fi
errors=()
# Subject ≤ 50 characters
len=${#subject}
if [ "$len" -gt 50 ]; then
errors+=("Subject is $len chars (max 50)")
fi
# No trailing period
if [[ "$subject" == *. ]]; then
errors+=("Subject must not end with a period")
fi
# Starts with a capital letter
if [[ ! "${subject:0:1}" =~ [A-Z] ]]; then
errors+=("Subject must start with a capital letter")
fi
# If a body is present, it must be separated by a blank line
body=$(printf '%s' "$msg" | tail -n +2)
if [ -n "$body" ]; then
second=$(printf '%s' "$msg" | sed -n '2p')
if [ -n "$second" ]; then
errors+=("Separate subject from body with a blank line")
fi
# Body lines ≤ 72 characters
line_num=0
while IFS= read -r line; do
line_num=$((line_num + 1))
[ $line_num -le 2 ] && continue # subject and blank separator
line_len=${#line}
if [ "$line_len" -gt 72 ]; then
errors+=("Line $line_num exceeds 72 chars ($line_len): \"$line\"")
fi
done <<< "$msg"
fi
# No AI attribution lines
if printf '%s\n' "$msg" | grep -qiE '^Co-Authored-By:.*(claude|anthropic|codex|openai|chatgpt|gpt-|copilot|gemini|cursor|devin)'; then
errors+=("Remove AI-generated attribution line (Co-Authored-By)")
fi
if [ ${#errors[@]} -gt 0 ]; then
echo "Commit message errors:"
for e in "${errors[@]}"; do
echo " • $e"
done
exit 1
fi
exit 0
I also provide the hook as a gist for easy copy-paste:
https://gist.github.com/timcase/71dfd39ca756fb6303657b173ea80351