Conventional Commits: A Practical Guide

RedHub AI Editorialupdated October 4, 20265 min read

A steel flat-file cabinet with one drawer pulled open and crammed with crumpled sheets at every angle, lit red.
Jump to a section7

Every message written to Conventional Commits 1.0.0, the current version of the specification, starts the same way: a type, an optional scope in parentheses, a colon and a description, as in fix(auth): stop token refresh racing logout. The spec itself defines only two types, feat and fix, and marks a breaking change with a ! before the colon or a BREAKING CHANGE: footer. The payoff is a history that release tools can read to pick the next version number and write the changelog.

TL;DR: Write type(scope): description. Use feat for features and fix for bug fixes. Other types such as docs, refactor and chore are conventions the spec allows. Flag breaking changes with ! or a footer. If your team squash-merges, the PR title is usually what lands on main, so it needs the format too. The Codex Code-Review & PR-Hygiene Pack ($79) includes a skill that normalizes messages and splits a mixed change into separate commits. For the wider workflow, read the AI code review guide.

The format

  • type: the kind of change, such as feat or fix.
  • (scope): optional, naming the area affected, such as (auth) or (api).
  • : description: a short summary. The spec says it must come immediately after the colon and a space.

A longer body and footers can follow after a blank line. The spec says tools must not treat these parts as case sensitive, with one exception: BREAKING CHANGE must be uppercase. Lowercase types are a team convention, not a rule.

Which types the spec defines

Only two. feat adds a feature and fix patches a bug. The spec allows any other type, and notes that @commitlint/config-conventional, based on the Angular convention, recommends build, chore, ci, docs, style, refactor, perf and test. Most teams use some of that list. The difference matters when a tool reads the history:

CommitWhat the spec says it means for the version
fix:A PATCH release
feat:A MINOR release
Any type with ! or a BREAKING CHANGE: footerA MAJOR release
docs:, refactor:, chore: and other typesNo implicit effect, unless a breaking change is marked

Marking a breaking change

Put a ! right before the colon, as in feat(api)!: drop the v1 orders endpoint, or add a BREAKING CHANGE: footer that says what breaks and what a consumer must do. The spec treats BREAKING-CHANGE as a synonym in a footer. Either way, it is the one marker that forces a major version, so it should be stated by the author, never left for a reviewer to notice in the diff.

When squash merging rewrites your history

Careful commits often go missing at merge time. Many teams squash-merge pull requests to keep main tidy, and GitHub's docs describe what the default message becomes. A PR with one commit keeps that commit's title and message. A PR with two or more commits gets the pull request title and a list of the commits underneath.

So on a squash-merge team, the PR title is the commit message a release tool reads. Three well-typed commits, a feat, a fix and a docs, collapse into whatever the title says. A title like "Checkout updates" carries no type at all, so a release tool reading types finds no feat to bump the version for.

There is no setting that keeps both benefits. Squashing gives you one readable commit per change on main. Keeping the commits gives you a history git bisect can walk step by step. Pick one on purpose, and if you squash, check the PR title against the format as well as the commits.

Turning a messy commit into a clean one

  1. Read the diff, not your memory. The type and scope should describe the change that is staged.
  2. Pick the type for the main intent. A fix with a small doc tweak is still fix.
  3. Split unrelated changes. A feature and an unrelated cleanup become two commits, each with an accurate type, instead of one commit that half-describes both.
  4. Say what breaks. Add the ! or the footer whenever a public signature, API or contract changes.

Style rules such as an imperative subject ("add", not "added") or a 50-character limit are common conventions, not part of the spec. Pick yours and let a tool apply them, so nobody has to remember. The PR hygiene checklist covers the other checks worth automating alongside it.

Clean commits, derived from the diff

The Codex Code-Review & PR-Hygiene Pack's Commit Message Normalizer rewrites messages to Conventional Commits and proposes how to split a mixed staged change into logical commits. It derives type and scope from the diff and never silently rewrites your history. A runnable PR-hygiene gate and four more review skills come with it.

Get the Codex Code-Review & PR-Hygiene Pack — $79

Pairs well with

Typed commits feed a changelog, and the Technical Documentation Engine ($99) writes changelogs alongside READMEs, API references and ADRs, with an auditor that returns DOCUMENTED or GAPS. Not every breaking change is loud enough to earn its !: the Ontology Change-Impact Tripwire ($79) grades data-model changes and ranks SILENT BREAK, where everything still runs but answers quietly change, above BREAKING. For commits on inherited code, the Codex Migration & Refactor Pack ($99) handles framework upgrades and dead-code removal with a verifier that returns SHIP or HOLD.

More in this guide

What is the Conventional Commits format?

type(scope): description. A type such as feat or fix, an optional scope in parentheses, then a short description right after the colon and a space. A body and footers can follow after a blank line.

Which commit types does the spec define?

Only feat and fix. Other types are allowed, and the spec notes that @commitlint/config-conventional recommends build, chore, ci, docs, style, refactor, perf and test. Those are conventions, not part of the spec's version rules.

How do you mark a breaking change?

Put a ! right before the colon, or add a BREAKING CHANGE: footer describing what breaks and what a consumer should do. The footer token must be uppercase. A breaking change maps to a major version.

Does squash merging break Conventional Commits?

It changes which text matters. On GitHub, squash-merging a PR with two or more commits uses the PR title as the commit title by default, so the title needs the format. A single-commit PR keeps that commit's own message.

What should happen to a commit that mixes a feature and an unrelated cleanup?

Split it into two commits, each with its own accurate type, instead of forcing one type onto a change it only half describes.

Is scope required in a Conventional Commit?

No. fix: stop token refresh racing logout is valid without a scope. Adding one, as in fix(auth):, just says where the change applies.

How it decides
Diagram of the Ontology Change-Impact Tripwire: schema changes rolled up worst-not-average, a silent-break escalation gate, and the release reading SILENT BREAKS PRESENT.

The gate this post refers to, drawn from the tool’s own logic. See the tool.