Stop Writing "Fixed Bug" as Your Commit Message
by Eric Hanson, Backend Developer at Clean Systems Consulting
The Problem With "Fixed Bug"
You're in the middle of an incident at 11pm. Something broke in production, you don't know when, and you need to find the commit that caused it. You run git log --oneline and you see this:
f3a1c2d fix
e72b931 Fixed bug
c14a882 update
b9f301e Fixed bug
a2c74f1 wip
991ba44 Fixed bug
Congratulations — you've just discovered that git log is useless on this project. You're going to have to read actual diffs to find the problem. That's going to take twenty minutes minimum, for a history that could have told you in two.
This is the consequence of "fixed bug" commit messages. Not the one time you wrote it in a hurry — the pattern, repeated across a team, across months, until the commit history becomes archaeological rubble.
Why It Happens
The "fixed bug" habit comes from treating commits as saves rather than documentation. When you're deep in a problem and finally crack it, the last thing you want to do is context-switch to writing prose about what you just did. You know what you fixed. You commit and move on.
The failure is not lack of care. It's lack of discipline around who the commit message is written for. It is not for you right now. It is for you in six months when you've forgotten the context. It is for your colleague who picks up the thread when you're on leave. It is for whoever runs git blame on this line in three years during an audit.
What a Commit Message Actually Needs
A commit message has two parts: the subject line (72 characters max, present tense, imperative mood) and an optional body separated by a blank line.
The subject line answers: what does this commit do?
The body answers: why was this necessary, and what context matters?
# Bad — says nothing
Fixed bug
# Slightly better — identifies what
Fix null pointer in OrderService
# Good — identifies what and why
Fix null pointer in OrderService for guest checkouts
Guest sessions don't always have an authenticated userId. The
getUser() call on line 47 assumed non-null and threw NPE when
a guest user initiated checkout. Added null check with fallback
to anonymous user context.
Fixes #1923.
That last example takes maybe ninety seconds to write. It will save someone hours someday — possibly you.
The Imperative Mood Rule
Git itself uses imperative mood in its auto-generated messages: "Merge pull request", "Revert commit", "Initial commit". There's a good reason for this: a commit message should complete the sentence "If applied, this commit will..."
- "Fix null pointer in OrderService" ✓ — If applied, this commit will fix null pointer in OrderService
- "Fixed null pointer" ✗ — reads like a past-tense status report
- "Fixing null pointer" ✗ — progressive tense, reads like a work-in-progress note
- "Null pointer fix" ✗ — not a sentence
This rule matters more when commits appear in automated changelogs, release notes, or tooling that processes commit messages. Conventional Commits (a specification worth adopting) builds on this with typed prefixes:
feat: add OAuth2 support for partner API integrations
fix: correct rounding error in tax calculation for EU orders
perf: add index on order_items.created_at to fix slow reports
refactor: extract payment gateway logic into dedicated service
chore: update Spring Boot from 3.1 to 3.3
docs: document retry behavior in PaymentService
The type prefix makes automated changelog generation trivial and gives reviewers instant context about the nature of the change.
What "Bug" Doesn't Tell Anyone
The word "bug" is not specific. Every fix fixes a bug by definition. Compare:
# Useless — the word "bug" carries zero information
Fixed bug in payment service
# Useful — names the failure mode, the location, the trigger
Fix duplicate charge on payment retry when gateway times out
Payment gateway occasionally returns a timeout after processing
a charge successfully. Our retry logic was treating the timeout
as a failure and submitting a second charge. Added idempotency
key (order_id + attempt_number) to prevent duplicate charges.
Reproducer: PaymentServiceTest#duplicateChargeOnRetry
Fixes #2041.
The "bug" version means you have to open the diff, understand the code change, and infer what behavior it was fixing. The specific version gives you the entire picture in thirty seconds.
Enforcing This Without Being a Bureaucrat
You cannot enforce good commit messages through code review alone — by the time the PR is up, the history is written. The practical approaches:
Commit-msg hook — runs a validation script on every commit message before it's accepted. A simple regex check ensures messages aren't just one word and are over a minimum length:
#!/bin/sh
# .git/hooks/commit-msg
MSG=$(cat "$1")
MIN_LENGTH=10
if [ ${#MSG} -lt $MIN_LENGTH ]; then
echo "Error: Commit message too short. Describe what and why."
exit 1
fi
if echo "$MSG" | grep -qiE '^(fix|update|wip|temp|asdf|test)$'; then
echo "Error: Commit message too vague. Be specific."
exit 1
fi
Install this via a tool like Husky for Node.js projects or a pre-commit config for Python projects, so it's shared across the team rather than living in each developer's local .git/hooks.
Squash on merge — if your PR workflow squashes commits at merge time, a well-written PR title becomes the commit message. This shifts the discipline to PR description rather than individual commits. It works, but you lose granularity in the history.
Conventional Commits + commitlint — the commitlint tool enforces the Conventional Commits spec at the commit-msg hook level. Zero ambiguity, machine-readable output, automatic changelogs.
The one thing that does not work: periodic lectures in team meetings. The habit only changes when the feedback loop is immediate — which means automated tooling at commit time.
The Standard to Hold Yourself To
Before you push, read each commit message and ask: if I saw this message without any other context, would I know what changed and why? If the answer is no, git commit --amend or git rebase -i and fix it.
"Fixed bug" is a lie by omission. You know exactly what you fixed and why. The message just didn't say it.