What Conventional Commits Actually Buys You
"Conventional Commits" is a lightweight specification for commit message structure: type(scope): description, where type is one of a small fixed set of words like feat, fix, or chore. It looks like a stylistic nicety, but it unlocks real automation that ad-hoc commit messages can't support.
- Automated changelogs. Tools like semantic-release can generate a changelog directly from commit history, grouping entries by type automatically.
- Automated version bumps. A
fix:commit can trigger a patch release, afeat:commit a minor release, and a breaking change (marked with!or aBREAKING CHANGE:footer) a major release — all without a human deciding the version number. - Faster code review. A reviewer scanning
fix(auth): handle expired refresh tokensinstantly knows the intent and scope before opening the diff. - Better git history. Six months later,
git log --onelinebecomes a genuinely useful record of what changed and why, not a wall of "fix stuff" and "wip".
The Rules, Distilled
The header line has three required parts and one optional marker:
- Type — one of feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.
- Scope (optional) — the area of the codebase affected, in parentheses:
(auth),(api),(deps). - Breaking marker (optional) — a
!immediately after the type/scope, signaling a breaking API change. - Description — a short, lowercase, imperative summary with no trailing period, ideally keeping the whole header under about 72 characters.
Breaking changes can also be declared in the commit body via a BREAKING CHANGE: footer, which is useful when the header alone can't fully explain the impact.
Common Violations Worth Catching Automatically
- Capitalized descriptions:
Fix: Corrected Buginstead offix: correct bug - Trailing periods:
fix: correct bug. - Invented types that aren't in the spec, like
update:orbugfix: - Missing the space after the colon:
fix:correct bug - Headers that run long enough to get truncated in git UIs and terminal output
Making Enforcement Actually Stick
Rules that live only in a CONTRIBUTING.md file get forgotten within a week. The rules that stick are the ones enforced at commit time — a local commit-msg hook, a CI check on pull requests, or simply a habit of running every header through a linter before committing. Catching a malformed header before it lands in history is far cheaper than trying to clean up commit messages retroactively, since git history is technically immutable in any branch others have already pulled.
The format itself is simple enough to learn in five minutes. The value comes entirely from consistency — one team member's feat: needs to mean the same thing as everyone else's for the automation built on top of it to actually work.
Handling Breaking Changes Correctly
The breaking-change marker is the part of the spec teams most often get wrong, usually by forgetting it exists at all. A ! placed immediately after the type (and scope, if present) — as in feat(api)!: replace REST endpoints with GraphQL — tells any automated release tooling to treat this as a major version bump, regardless of the type otherwise being a minor-triggering feat. The same signal can be communicated in the commit body via a BREAKING CHANGE: footer, which is the better choice when the header alone can't convey what actually broke and why.
A commit can use both simultaneously — the ! for a quick visual signal in the header, and a detailed BREAKING CHANGE: footer explaining exactly what downstream consumers need to change. This is the recommended pattern for any change that removes a public API, alters a function's parameters, or changes a default behavior that other teams depend on. Omitting the marker on a genuinely breaking change is worse than a purely stylistic violation — it means semantic-release tooling ships the change as a minor or patch version, and consumers who trust semantic versioning to warn them before upgrading get blindsided.
Conversely, marking something as breaking when it isn't erodes trust in the signal over time, training reviewers and downstream teams to ignore it. Reserve the marker for changes that genuinely require action from someone consuming your code — not for internal refactors that happen to touch a lot of files.