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.

In this post
  • The entire format in one line
  • The types you'll actually use — and what chore really means
  • Scope, the 50-character rule, and the imperative mood
  • Breaking changes: the ! and the BREAKING CHANGE footer
  • A copy-paste cheat sheet for your next PR

The whole spec in one line

The entire format fits on a single line:

the-format.txt
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:

the-types.txt
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 not a dumping ground

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 auth today, don't make it authentication tomorrow.
  • Skip it when it's obvious. feat: add dark mode is fine for a solo project; feat(theme): add dark mode is 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.
subjects.txt
  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
The fill-in-the-blank test

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:

breaking-change.txt
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.

Unmarked breaking changes lie to your tooling

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 — feat entries become Features, fix entries become Bug Fixes, no human collation.
  • Semantic versioning for free. feat bumps minor, fix bumps patch, BREAKING CHANGE bumps 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 --oneline tells you what changed and where — feat(cart): recalculate totals is 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:

cheat-sheet.txt
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.