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.
Frequently Asked Questions
It's a lightweight structure for commit message headers: type(scope): description, where type is one of a small fixed set of words — feat, fix, docs, style, refactor, perf, test, build, ci, chore, or revert — and scope is an optional area of the codebase in parentheses like (auth) or (api). The description should be a short, lowercase, imperative summary with no trailing period, ideally keeping the whole header under about 72 characters.
Add a ! immediately after the type (and scope, if present), as in feat(api)!: replace REST endpoints with GraphQL — this signals automated release tooling to trigger a major version bump regardless of the type otherwise being a minor-triggering feat. You can also, or instead, add a BREAKING CHANGE: footer in the commit body, which is the better choice when the header alone can't explain what actually broke; a commit can use both together for a quick visual signal plus full detail.
The standard set is feat, fix, docs, style, refactor, perf, test, build, ci, chore, and revert. Inventing your own types like update: or bugfix: is one of the most common violations teams run into, since it breaks the consistency that automated tooling — changelog generation, semantic-release version bumps — depends on to interpret every commit the same way.
Beyond tidiness, a consistent format unlocks real automation: tools like semantic-release can generate a changelog directly from commit history grouped by type, and can decide version bumps automatically — a fix: commit triggers a patch release, a feat: commit a minor release, and a marked breaking change a major release — all without a human deciding the version number. It also makes git log --oneline six months later an actually useful record instead of a wall of 'fix stuff' and 'wip' entries.
Yes — a Conventional Commit Linter checks a message against the spec and flags the common violations (capitalized descriptions, trailing periods, invented types, a missing space after the colon, an overly long header) before it lands in history. Rules that only live in a CONTRIBUTING.md file get forgotten within a week, but catching a malformed header at commit time is far cheaper than trying to clean up commit messages retroactively, since git history is effectively immutable once others have pulled it.