Open the git log of most projects and you find a wall of “fix stuff”, “wip”, and “update again”. It is history in name only. Conventional Commits is a convention for the first line of a commit message that fixes this by giving each commit a machine-readable prefix. The payoff goes past tidiness. Once a machine can read your commits, it can generate your changelog, decide your next version number, and let you filter a year of history down to every breaking change in one command.
The specification is currently at version 1.0.0 and it is short enough to read in ten minutes. What follows is the format itself, then the parts people get wrong, then the honest tradeoffs around adopting it on a real team.
The format
Every conventional commit follows one structure. The type and description are required, the rest are optional:
Read left to right. The type is a noun like feat or fix. The scope in parentheses is optional and names the part of the codebase you touched, such as (parser) or (api). The ! is optional and flags a breaking change. Then a colon, a single space, and a short description in the imperative mood. Here is the everyday case with no ceremony:
The two types the spec actually defines
This is the part most explainers blur, so it is worth stating precisely. The Conventional Commits specification defines the meaning of exactly two types, and it ties them directly to semantic versioning:
- fix: a commit that patches a bug. It correlates with a PATCH release (the third number, as in 1.4.2).
- feat: a commit that introduces a new feature. It correlates with a MINOR release (the middle number, as in 1.5.0).
That is the whole contract the spec makes about release semantics. A tool reading your history can bump the version correctly from those two types alone, plus one more rule for breaking changes.
Breaking changes drive the MAJOR bump
A breaking change is any commit that would force a consumer of your code to change theirs. The spec gives you two ways to mark it, and either one on its own is sufficient. The first is an exclamation mark right before the colon:
The second is a footer that starts with the exact token BREAKING CHANGE: in uppercase, followed by a description of what broke and how to migrate. This form gives you room to explain:
Note that a breaking change can ride on any type at all. A fix! or a refactor! that changes a public contract still forces a MAJOR bump. The signal is the ! or the footer, and the type it rides on is irrelevant.
The extended types everyone actually uses
Beyond feat and fix, the spec permits any type you want and stays silent on their meaning. In practice a small, near-universal vocabulary has settled, mostly inherited from the Angular commit convention. These types leave the version alone and earn their keep by making history far easier to scan:
- docs: documentation only, no code change.
- refactor: a code change that neither fixes a bug nor adds a feature.
- test: adding or correcting tests.
- chore: maintenance that does not touch src or tests, like bumping a dependency.
- build: changes to the build system or external dependencies.
- ci: changes to CI configuration and scripts.
- perf: a change that improves performance.
- style: formatting, whitespace, semicolons, nothing that affects meaning.
The advice buried in that list: keep the set small. A team that invents twenty types spends its energy debating whether a change is a chore or a build instead of shipping. Nine types cover almost everything.
Body and footer rules
The body is free-form prose. It begins one blank line after the description and explains the why, the context a reviewer or a future maintainer needs that does not fit in a one-line summary. The spec asks only that a blank line separate the description from the body.
Footers come last, each on its own line, in a token: value shape borrowed from git trailers, such as Reviewed-by: Kim or Refs: #99. The one footer with defined meaning is BREAKING CHANGE:. As a convenience the spec allows BREAKING-CHANGE with a hyphen as a synonym, since a hyphen is a valid token character and a space is not.
Why bother: the three payoffs
The format earns its keep through what it unlocks, and that comes down to three concrete wins.
Automated changelogs and releases
Tools like semantic-release and release-please read the commit types since the last tag, compute the correct next version, generate a categorised changelog, tag the release, and publish it, with no human deciding the number. Your commit messages become the release notes.
A greppable history
A structured prefix means you can query your own history. Every breaking change since a tag, every performance commit, every fix that touched the parser, all one command away:
A shared vocabulary
The subtle win is human. When a whole team writes fix: and feat:, a reviewer knows the stakes of a diff before reading a line of it, and a newcomer can read the log as a narrative rather than noise.
Adopting it on a real team
The convention only pays off if the log is consistent, and consistency across a team comes from tooling that checks messages before they land. A practical rollout looks like this:
- Add commitlint with the config-conventional preset and wire it to a commit-msg hook via husky, plus a CI check. A malformed message fails fast, locally, before it ever reaches the shared history.
- Agree the type list in one short document and resist adding to it. Fewer types, fewer arguments.
- Let a tool decide versions. Once the log is clean, hand the version bump and changelog to semantic-release so no one relitigates whether a change was minor or patch.
A minimal commitlint.config.js is all the enforcement most teams need:
The honest criticisms
Conventional Commits is not free, and pretending otherwise is how it gets resented. Two objections come up repeatedly and both are fair.
The first is friction. Prefixing every commit feels like bureaucracy until the automation pays it back, and if you never turn on the automation, you have added ceremony for nothing. Only adopt the format if you intend to use what it unlocks.
The second is squash-merge. Many teams squash each pull request into a single commit on the main branch, which throws away the individual commit messages the convention so carefully structured. The fix is to move the convention up a level: apply it to the pull request title instead. GitHub can lint the PR title and use it verbatim as the squash commit message, so you keep a clean, parseable main-branch history while your working commits stay as messy as you like. A squash workflow is a reason to enforce the convention on the PR, not to skip it. A good pull request template is the natural place to remind people of the format.
A cheat sheet to keep
One block that covers ninety percent of what you will write. Copy it, pin it, and the format stops needing thought:
That is the whole convention. The rest is habit, and a good habit here compounds: cleaner history, version numbers that mean something, and a changelog that writes itself. When you have the numbers straight, the next piece is what those version digits actually promise, which is where semantic versioning comes in, and both fit inside the broader habit of shipping a repo that is pleasant to work in, alongside a solid README.
Frequently asked questions
What are the only two commit types defined by the Conventional Commits spec?
The specification itself names exactly two: fix, which correlates with a PATCH release in semantic versioning, and feat, which correlates with a MINOR release. Everything else, docs, chore, refactor, test, ci, build, perf and style, is convention layered on top by teams and tools like Angular. The spec allows any type you like, it just gives feat and fix a defined meaning.
How do you mark a breaking change in a conventional commit?
Two ways, and either is enough. Put a ! right before the colon, as in feat!: drop Node 18 support, or add a footer that begins with BREAKING CHANGE: followed by a description. A breaking change triggers a MAJOR version bump regardless of whether the type is feat, fix or anything else.
Do Conventional Commits require a specific tool?
No. It is a plain-text convention, so you can follow it by hand in any editor. Tools are optional and only add enforcement or automation: commitlint checks messages in CI, commitizen prompts you through the format, and semantic-release reads the history to decide the next version and write the changelog.
Do Conventional Commits work with squash-and-merge?
Yes, but the useful unit shifts. If your team squashes each pull request into a single commit, individual commit messages get discarded, so the convention should live on the PR title instead. GitHub can lint the PR title and use it as the squash commit message, which gives you the same parseable history at the merge level.