Commit messages are not administrative noise. They are part of the engineering record.
When a system has been running for years, the code alone often tells you what changed but not why. The pull request may be gone from memory, the people may have moved on, and the business constraint that shaped the implementation may no longer be obvious.
That is when a good commit message saves time.
Code explains the current state
The code is the source of truth for behavior. It is not always the source of truth for intent.
Good engineers still need to know why an awkward tradeoff was accepted, why a simpler approach was rejected, or why a change was split across multiple files. Without that context, later work becomes guesswork.
That guesswork is expensive. It slows review, causes avoidable rewrites, and can reintroduce bugs that were already understood once.
What a useful commit message answers
A good commit message usually answers three questions:
- why the change exists
- what approach was taken
- what side effects or constraints matter
The body does not need to be long. It needs to preserve the reasoning that will not be obvious from the diff.
Reject expired invitations during signup
Invitations can be accepted after the token expires because the signup
flow only checks whether the token exists. This lets old invitations create
accounts after access has been revoked.
Validate the invitation expiry before account creation and show the same
error used for invalid tokens. Keep the token lookup unchanged so existing
audit logs still point to the attempted invitation.
That message gives a future reader more than a title. It explains the failure mode, the fix, and the compatibility constraint.
Small commits make better history
Commit messages are easier to write when commits are coherent.
If one commit updates dependencies, changes a database query, fixes copy, and reformats a file, the message will either become vague or become a small essay. Both are signs the commit is doing too much.
The habit I want is simple: one reason per commit.
That does not mean every commit must be tiny. It means the change should have one defensible purpose.
Format helps, but it is not the point
Conventions such as an imperative summary, a blank line before the body, and wrapped lines are useful because they make history easier to scan in terminals and hosting tools.
They are not the main point. A perfectly formatted message that says “misc fixes” still fails.
The main point is operational memory. The team should be able to inspect history and recover the reasoning without turning every investigation into archaeology.
Force with care
Interactive rebase, squash, and fixup are good tools before a branch is shared or before a pull request is merged. They help turn exploratory work into readable history.
Once a branch is shared, use force carefully. Prefer --force-with-lease so you do not overwrite someone else’s work by accident.
The standard is not purity. The standard is respect for the next person who has to understand the system.
Addendum: Conventional Commits
Conventional Commits are a useful layer on top of good commit messages. They define a simple prefix format:
feat(auth): add passkey enrollment
fix(billing): reject expired trial coupons
docs(api): document pagination limits
The prefix is not enough on its own. A consistent prefix can drive changelog generation, release notes, semantic versioning, and CI workflows.
That does not remove the need for a body when the change needs explanation. A fix: summary tells the reader the kind of change. It does not explain the constraint, rejected alternative, or operational risk.
My default rule is:
- use Conventional Commits when the project benefits from automated releases or structured history
- still write a body when the reasoning matters
- avoid turning types into taxonomy arguments
The prefix is metadata. The body is where the engineering context lives.
Useful reference: Conventional Commits 1.0.0.
