Enforcing subject line length and format Jump to heading
The commit subject is the most-read line of any commit. It appears in git log --oneline, in git blame annotations in editors, in rebase todo lists, in pull request lists, in release tooling and in email patches. Tools assume it is short: many truncate around 50 to 72 characters, and Git itself treats everything up to the first blank line as the subject, so a missing blank line turns a whole paragraph into an unreadable “subject”. A handful of mechanical rules — a length limit, a blank second line, wrapped body lines, an imperative verb — keep every one of those views readable. This page sets out the rules and enforces them with a hook and in CI, within commit message hooks and templates.
When to use this approach Jump to heading
git log --onelineon main is full of truncated or multi-sentence subjects.- Release notes or changelogs are generated from subjects and look ragged.
- Messages sometimes have no blank line between subject and body.
- You use a structured format such as Conventional Commits; the structure is enforced in how to enforce Conventional Commits with commitlint.
Step 1 — Agree the rules Jump to heading
Keep the list short and mechanical, so a script can check it and nobody argues about taste.
The imperative rule (“Add retry to export client”, not “Added” or “Adds”) matches the messages Git itself generates — “Merge branch”, “Revert” — and reads naturally as “this commit will…”.
Step 2 — Check the rules in a commit-msg hook Jump to heading
The hook receives the message file. Strip comment lines (Git removes them anyway) before checking.
#!/bin/sh
# scripts/hooks/check-subject.sh <message-file>
set -eu
msg=$(sed '/^#/d' "$1")
subject=$(printf '%s\n' "$msg" | sed -n '1p')
line2=$(printf '%s\n' "$msg" | sed -n '2p')
case "$subject" in Merge\ *|Revert\ \"*|fixup!*|squash!*|amend!*) exit 0 ;; esac
err=""
[ "${#subject}" -le 72 ] || err="$err\n subject is ${#subject} characters (max 72)"
[ -z "$line2" ] || err="$err\n line 2 must be blank"
case "$subject" in *.) err="$err\n subject ends with a period" ;; esac
printf '%s\n' "$subject" | grep -Eq '^([a-z]+(\([a-z0-9-]+\))?!?: )?[A-Z]?[a-z]+' \
|| err="$err\n start with a verb or a type prefix"
printf '%s\n' "$msg" | sed '1,2d' | grep -vE 'https?://|^ ' | awk 'length > 72 {bad=1} END {exit bad}' \
|| err="$err\n wrap body lines at 72 characters"
[ -z "$err" ] || { printf "Commit message problems:$err\n" >&2; exit 1; } # .husky/commit-msg
sh scripts/hooks/check-subject.sh "$1" Step 3 — Help people write good messages, not just reject bad ones Jump to heading
A template that shows the shape, and an editor configured to show the 50 and 72 columns, prevents most failures before the hook runs.
cat > .gitmessage <<'EOF'
# Subject: imperative, ≤ 50 chars ideally, ≤ 72 max, no trailing period
# e.g. "Add retry with backoff to export client"
#
# Body: why the change was needed and anything surprising. Wrap at 72.
#
# Trailers: Fixes: PAY-812 Co-authored-by: Name <email>
EOF
git config commit.template .gitmessage Per-branch templates, which prefill an issue key from the branch name, are covered in generating a commit message template per branch.
Step 4 — Enforce the same rules in CI Jump to heading
The hook can be skipped, so CI runs the same script over every commit in a pull request. Writing each message to a file lets the script be reused unchanged.
- run: |
base=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
fail=0
for c in $(git rev-list --no-merges "$base..HEAD"); do
git log -1 --format=%B "$c" > /tmp/msg
sh scripts/hooks/check-subject.sh /tmp/msg 2>/tmp/err || {
echo "::error::$(git log -1 --format='%h %s' "$c")"; cat /tmp/err; fail=1; }
done
exit $fail Step 5 — Check the pull request title too Jump to heading
With squash merging, the pull request title usually becomes the commit subject on main. Apply the same rules to the title, so the merged commit meets them even though individual commits were never checked.
- name: Pull request title follows the subject rules
env: { TITLE: "${{ github.event.pull_request.title }}" }
run: printf '%s\n' "$TITLE" > /tmp/title && sh scripts/hooks/check-subject.sh /tmp/title Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Why 72 characters and not 50? Jump to heading
Fifty fits comfortably everywhere and is a good target. Seventy-two is where most tools start truncating or wrapping awkwardly, so it makes a sensible hard limit that rarely forces awkward abbreviations.
Can the hook check for the imperative mood properly? Jump to heading
Only approximately. A list of common non-imperative openings — “Added”, “Fixes”, “Updated” — catches most cases. Full grammatical checks produce false positives and annoy people more than they help.
Should bot commits follow the rules? Jump to heading
Configure bots to produce compliant messages; most allow a commit message prefix and template. Exempting bots by author is a fallback, not a first choice.
Related Jump to heading
- Commit Message Hooks & Templates — the parent topic.
- Writing a Good Squash Commit Message — the squash-merge case in depth.
- Enforcing Commit Message Policy in pre-receive — the server-side equivalent.
- Rejecting WIP and Fixup Commits Before Merge — the other check on preserved commits.