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 --oneline on 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.

Five mechanical message rulesThe subject stays within 72 characters, or 50 as a softer target. The second line is blank so tools can tell subject from body. The subject has no trailing period. It starts with an imperative verb or a structured type prefix. Body lines wrap at 72 characters, except URLs and code.≤ 72 chars50 is the targetBlank line 2subject ≠ bodyNo final '.'it is a titleImperativeAdd, Fix, RemoveBody ≤ 72URLs exemptevery rule here can be checked by a few lines of shell

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
A message that fails the rules and one that passesThe failing message puts a long sentence on the first line, has no blank second line and ends with a period, so log views truncate it and tools treat the whole paragraph as the subject. The passing message has a short imperative subject, a blank line and a wrapped body.FailsPassessubjectAdded retries because exports were…Add retry with backoff to export clientlength118 characters39 charactersline 2body starts immediatelyblankends witha periodno periodthe passing subject also reads well in a release note and in blame

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
Where to enforce subject rulesIf commits are preserved on merge, check every commit with the commit-msg hook and in CI. If pull requests are squash-merged, check the pull request title, which becomes the subject on main. If both happen in your organisation, do both.How do changes land on main?commits preservedCheck each commithook + CIsquash mergeCheck the PR titleit becomes the subjectmixedCheck bothsame scriptpassing the title through the same script keeps one definition of 'good'

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.