Conventional Commits cheat sheet
Every codebase has two kinds of commit messages: "update stuff," and the
forty-minute existential crisis over whether a change is a "fix" or a "feat."
Conventional Commits is the cure for both — a tiny grammar that makes your
history readable, your changelogs automatic, and your 2am
git log actually useful.
Here's the whole system, including the parts people get wrong: the imperative mood, the 50-character rule, and the breaking-change bang.
- The entire format in one line
- The types you'll actually use — and what
chorereally means - Scope, the 50-character rule, and the imperative mood
- Breaking changes: the
!and theBREAKING CHANGEfooter - A copy-paste cheat sheet for your next PR
The whole spec in one line
The entire format fits on a single line:
feat(auth)!: add two-factor authentication
│ │ │ └─ description: imperative, ~50 chars, no period
│ │ └─ ! = breaking change (optional)
│ └─ scope: the part of the codebase (optional)
└─ type: feat · fix · chore · docs · refactor · test · …
That's the spec. A type, an optional scope in parentheses, an optional
!, a colon, and a short description. Optionally, a body and a
footer after blank lines. The official spec is short enough to read in one
coffee — which is exactly the point. It's a convention, not a framework.
The types you'll actually use
The spec only mandates two types — feat and fix.
Everything else is convention, but a widely supported one. Here are the
eleven you'll see in the wild:
type use it for
──────────────────────────────────────────────────
feat a new feature for the user
fix a bug fix
chore deps, config, tooling — no app-logic change
docs documentation only
style formatting, whitespace — no logic change
refactor restructure code, no behavior change
perf a change that improves performance
test adding or fixing tests
build build system or external dependencies
ci CI configuration
revert reverting a previous commit
Ninety percent of your commits will be the top three: feat
(new capability), fix (bug squashed), and chore
(everything that isn't product code — dependencies, config, tooling).
chore is for changes that don't touch application logic. If
your "chore" changes how the app behaves, it's a feat or a
refactor wearing a costume. Chores get skipped in release
notes — so a behavior change hiding in one is a behavior change nobody
reads about.
The rest earn their keep by being specific. When in doubt, ask "what did this change do?" and pick the type that answers that.
Scope — the optional part that's worth it
The scope is the part in parentheses — the area of the codebase the change
touches: feat(auth): fix(parser):
chore(deps):
Three rules:
- One lowercase word (or a short hyphenated pair).
- Be consistent within a project. If it's
authtoday, don't make itauthenticationtomorrow. - Skip it when it's obvious.
feat: add dark modeis fine for a solo project;feat(theme): add dark modeis better once the repo has sections worth naming.
Scopes are what make git log --oneline scan like a table of
contents instead of a noise feed.
The subject line rules
The subject — everything after the colon — has three rules that do all the work:
- Imperative mood. "add," not "added" or "adds." A commit message describes what applying the commit does — git itself writes "Merge branch…" this way.
- No trailing period. It's a headline, not a sentence.
- About 50 characters. Short enough to read in a log, long enough to say something.
✗ update stuff → no type, says nothing
✗ fix: Fixed the bug → past tense, vague
✗ feat: added user authentication. → past tense, trailing period
✓ feat: add user authentication
✓ fix(parser): handle empty input
✓ chore(deps): bump axios to 1.7.0
Stuck? Start with "If applied, this commit will…" and finish the sentence.
"If applied, this commit will add user authentication" →
feat: add user authentication. It forces the imperative mood
for free.
And lowercase the first word after the colon — feat: Add and
feat: add both exist in the wild, but lowercase matches the
spec's examples and reads cleaner in a log.
Breaking changes — the ! and the footer
When a change breaks existing users — a removed endpoint, a renamed config,
a dropped runtime — you mark it two ways. A ! after the type
(and scope, if there is one):
feat(api)!: drop support for Node 16
And a BREAKING CHANGE: footer that explains it:
feat(api)!: drop support for Node 16
The runtime team has moved everything to Node 20, and the
build pipeline no longer tests against 16.
BREAKING CHANGE: Node 16 is no longer supported. Upgrade to
Node 20+ before installing this version.
Closes #482
The ! is the machine-readable signal; the footer is the
human-readable explanation. Use both.
Semantic-versioning tools (semantic-release, standard-version) only bump
the major version when they see the ! or the footer. A
breaking change you forget to mark ships as a patch, breaks your users,
and makes your version numbers fiction.
The ! goes after the scope and before the colon —
feat(api)!: — which is the one bit of syntax people reliably
put in the wrong place.
Why bother — the payoff
"It takes longer" is the usual objection, and it's backwards. The format takes ten seconds per commit; the payoff compounds every day after:
- Automatic changelogs. Tools like standard-version and semantic-release read the types and write your release notes —
featentries become Features,fixentries become Bug Fixes, no human collation. - Semantic versioning for free.
featbumps minor,fixbumps patch,BREAKING CHANGEbumps major. Your version numbers stop being a vibes-based decision. - A greppable history.
git log --grep="^fix"lists every bug fix. Try that with "update stuff." - A 2am-friendly log. When production is down,
git log --onelinetells you what changed and where —feat(cart): recalculate totalsis a lead; "fixed some stuff" is a shrug.
The commit generator builds all of this live — pick a type, add a scope, toggle breaking, and it coaches the 50-character rule as you type.
The cheat sheet
The whole post on one card:
feat(auth): add two-factor authentication
fix(cart): recalculate totals when quantity changes
chore(deps): bump express to 4.19.0
refactor(api): extract pagination into middleware
docs(readme): add local development setup
test(parser): cover empty and malformed input
perf(query): batch database lookups
build(docker): use multi-stage builds
ci(github): run tests on pull requests
revert: feat(auth): add two-factor authentication
Screenshot it. The revert line is the one nobody remembers —
it's revert: followed by the original subject, verbatim.
Wrap-up
Type, optional scope, colon, imperative subject under 50 characters.
! for breaking changes, a footer to explain them. That's the
whole grammar — everything else is just picking the right type.
This is the last of the five starter posts. If you landed here first, the JWT decoding post is a good place to start, and the blog index has the full set. And the next time a commit message stalls you, open the commit generator — watching the preview assemble itself is the fastest way to learn the shape.