Is your commit message valid?
CommitCheck validates Conventional Commits format in real time, no Commitlint, no setup, no account.
How CommitCheck works
CommitCheck is a parser, not a spell checker. It takes your message apart the same way a release tool does, then reports every rule the parts break.
- 1
You type or paste a commit message
Validation runs on every keystroke, with no debounce and no network call. You can paste a full multi-line message: body and footer lines are preserved, but the rules below are applied to the header (the first line), because that is the line release tooling parses.
- 2
The header is split at the first colon
Everything before the first colon becomes the prefix (type, optional scope, optional breaking-change marker). Everything after it becomes the description. If there is no colon at all, validation stops immediately with a single error, because nothing else can be determined.
- 3
The prefix is broken into type, scope, and breaking flag
A trailing ! sets the breaking flag. A pair of parentheses is extracted as the scope. Whatever remains at the front is the type. This is why fix(api)!: parses cleanly into type fix, scope api, breaking true.
- 4
Each part is checked against the spec
The type must be one of the 11 lowercase types listed below. The scope, if present, must be non-empty and contain no spaces. The description must exist, and it is checked for capitalisation, a trailing period, and length.
- 5
Unknown types get a suggestion
Common near-misses are mapped to the right type, so feature suggests feat, bugfix and hotfix suggest fix, and documentation suggests docs. If there is no known mapping, a prefix match is attempted before the tool gives up.
- 6
Results are split into errors and warnings
Errors mean the message is not a valid Conventional Commit. Warnings mean it parses but breaks a common convention. When there are zero errors, the parsed breakdown (type, scope, breaking, description) is displayed so you can confirm the tool read your intent correctly.
What the validation result means
CommitCheck returns one of three states. The difference between the amber state and the red state matters: one will still drive your changelog, the other will be skipped entirely by release tooling.
The message parses cleanly and follows every convention the tool checks. The parsed breakdown appears underneath so you can confirm the type, scope, breaking flag, and description are what you intended. This is the state to aim for on any commit that will reach your main branch.
feat(auth): add refresh token rotation
Type: feat · Scope: auth · Breaking: No
Description: add refresh token rotationThe message parses, and semantic-release or release-please will happily read it. But it breaks a convention that most teams follow, such as a capitalised description or a trailing period. Commitlint's config-conventional preset turns several of these into hard errors, so treat warnings as things to fix rather than opinions to ignore.
feat: Add login page.
# ^ capitalised ^ trailing period
# Both are warnings here, both are errors under commitlint defaults.At least one rule makes the message unparseable as a Conventional Commit. The parsed breakdown is hidden, because there is nothing reliable to show. In a repo with automated releases, a commit in this state is silently dropped from the changelog and contributes nothing to the version bump.
feature: add login page
# 'feature' is not a valid type. Did you mean 'feat'?
fix:handle expired tokens
# Missing space after the colon.Every rule CommitCheck applies
Each row is a real check with the exact input that triggers it. Paste any example into the tool above to see the message it produces.
| Check | Severity | Example that triggers it |
|---|---|---|
| No colon in the header | Error | add login page |
| No space after the colon | Error | fix:handle null user |
| Type is not in the valid list | Error | feature: add login page |
| Type is not lowercase | Error | Feat: add login page |
| Scope parentheses are empty | Error | fix(): handle null user |
| Scope contains a space | Error | fix(auth service): handle null |
| Scope parenthesis never closes | Error | fix(auth: handle null user |
| Characters after the closing parenthesis | Error | fix(auth)x: handle null user |
| Description is empty | Error | feat: |
| Description starts with a capital | Warning | feat: Add login page |
| Description ends with a period | Warning | feat: add login page. |
| Description longer than 100 characters | Warning | feat: add a login page and also refactor the entire session layer while we are in here ... |
| Scope is not lowercase | Warning | feat(Auth): add login page |
Conventional Commits syntax reference
A full Conventional Commit has up to three parts: a required header, an optional body, and optional footers. Blank lines separate them, and that separation is what tools use to tell them apart.
type(scope)!: description
| | | |
| | | +-- short summary, lowercase, no trailing period
| | +---------- optional: marks a breaking change
| +--------------- optional: the area of the codebase touched
+--------------------- required: one of the 11 types below
<blank line>
Optional body. Free-form prose explaining why the change was
made. Can span as many lines and paragraphs as you need.
<blank line>
BREAKING CHANGE: description of what consumers must change
Refs: #482
Reviewed-by: Zeeshan Tofiqfeat: add dark mode toggle
feat(ui): add dark mode toggle
feat(ui)!: replace the theme API with CSS variables
fix(api-gateway): retry once on upstream 502
chore(deps): bump next from 15.4.0 to 16.0.0
revert: "feat(ui): add dark mode toggle"
perf(db): replace N+1 order lookup with a single join
docs: document the NEXT_PUBLIC_ prefix rules# Colons inside the description are fine: only the FIRST colon splits.
fix(parser): handle the case where key: value has no space
# Hyphens and dots are allowed inside a scope, spaces are not.
fix(auth-service): renew the session cookie # valid
fix(auth service): renew the session cookie # error
# The ! goes AFTER the scope, never inside or before it.
feat(api)!: drop the v1 endpoints # valid
feat!(api): drop the v1 endpoints # error
# A BREAKING CHANGE footer works without the ! marker.
feat: move config to a single file
BREAKING CHANGE: .apprc is no longer read. Move all keys
into app.config.ts before upgrading.The spec allows BREAKING-CHANGE (hyphenated) as a synonym for BREAKING CHANGE in footers, because Git trailers do not permit spaces in keys. Both are recognised by release tooling.
Valid commit types
Only feat and fix are defined by the spec itself. The other nine come from the widely used Angular convention, and they are what commitlint accepts out of the box.
featA new featurefixA bug fixdocsDocumentation only changesstyleFormatting and whitespace, no logic changerefactorCode change that is neither a fix nor a featuretestAdding or correcting testschoreMaintenance: deps, config, toolingperfA code change that improves performanceciChanges to CI configuration files and scriptsbuildChanges to the build system or dependenciesrevertReverts a previous commitOnly feat and fix bump a version by default. Everything else is recorded in history without changing the release number, unless it carries a breaking-change marker.
How to mark breaking changes
Two methods, both valid. The ! shorthand is more visible in git log; the footer works better for multi-line explanations.
Method 1: ! shorthand (recommended)
feat!: remove deprecated /auth/login endpoint
fix(api)!: change token format from JWT to opaque tokensMethod 2: BREAKING CHANGE footer (more detail)
feat: replace /auth/login with OAuth flow
BREAKING CHANGE: The /auth/login endpoint has been removed.
Clients must use /oauth/authorize instead.Either method triggers a major version bump in semantic-release and release-please. You can use both together when you want the marker visible in git log --oneline and the explanation available to changelog readers.
When to use CommitCheck
Each row maps a real situation to what you should paste into the tool and what part of the result to read first.
| Situation | What to paste | What to read |
|---|---|---|
| Your pre-commit hook rejected a commit | The exact message commitlint refused | The error list, which names the failing rule in plain language |
| Writing the commit that triggers a major release | feat(api)!: drop the v1 endpoints | The parsed breakdown, confirming Breaking reads Yes |
| Reviewing a squash-merge PR title | The PR title, which becomes the squashed commit | Whether the type is valid, since an invalid title skips the changelog |
| Teaching the format to a new teammate | The built-in bad type and no space examples | The suggestion text, which shows feature maps to feat |
| Auditing an existing repo before adopting semantic-release | Messages from git log --format=%s -n 50, one at a time | The count of invalid messages, which tells you the migration cost |
| Deciding whether a scope helps | The same message with and without a scope | The Scope field, plus how the header reads at a glance |
Why use Conventional Commits?
- Automated changelogs. Tools like release-please, semantic-release, and conventional-changelog read your commit history to generate CHANGELOG.md entries automatically.
- Semantic version bumps. feat commits bump the minor version. fix commits bump the patch. Breaking changes (! or BREAKING CHANGE footer) bump the major version, automatically.
- Readable git history. A consistent format makes it easy to scan what changed and why, especially when narrowing down a bug introduction with git bisect.
- Tooling compatibility. GitHub Copilot suggests it by default. Changesets, nx, turborepo, and most modern monorepo tools understand it natively.
- Cheaper release reviews. Grouping by type means a release reviewer can read every feat and fix first, then skim the chore and ci entries, instead of reading 200 unstructured subject lines.
Frequently Asked Questions
What is Conventional Commits?
A specification for structuring Git commit messages. Format: type(scope): description.
- Enables automated changelogs: tools read your commit history to generate
CHANGELOG.mdautomatically. - Drives semantic version bumps:
feat= minor,fix= patch,feat!= major. - Tools that depend on it:
semantic-release,release-please,conventional-changelog,Changesets.
How is CommitCheck different from Commitlint?
| CommitCheck | Commitlint | |
|---|---|---|
| Setup | Zero (browser tool) | Install + config file + Husky hook |
| Use case | Quick validation, learning the format | Team-wide enforcement in CI |
| Works offline? | Yes, all in-browser | Yes, local CLI |
| Blocks commits? | No | Yes, pre-commit hook |
Use CommitCheck to learn and validate. Use Commitlint to enforce the format for every developer on the team.
Is the scope required?
No. Scope is optional. Both of these are valid:
feat: add login page # no scope
feat(auth): add login page # with scopeUse scope when it helps readers understand which part of the codebase changed.
How do I mark a breaking change?
# Method 1: ! shorthand (recommended, visible in git log)
feat!: remove deprecated /auth/login endpoint
# Method 2: BREAKING CHANGE footer (better for detailed explanation)
feat: replace authentication with OAuth
BREAKING CHANGE: /auth/login has been removed. Use /oauth/authorize.What commit types are valid?
| Type | When to use |
|---|---|
| feat | New feature |
| fix | Bug fix |
| docs | Documentation only |
| style | Formatting, no logic change |
| refactor | Code restructure, not a fix or feature |
| test | Adding or correcting tests |
| chore | Maintenance (deps, config, tooling) |
| perf | Performance improvement |
| ci | CI config changes |
| build | Build system or dependency changes |
| revert | Reverts a previous commit |
Does CommitCheck work offline?
Yes. All validation runs entirely in your browser, and there are no API calls. Once the page loads, it works without an internet connection. Nothing you paste is sent anywhere or stored.
My team uses custom types like wip or deps. Will they pass?
No. CommitCheck validates against the 11 types listed above, which is what commitlint's config-conventional preset accepts by default. A custom type is reported as an error, with a suggested replacement when one is obvious.
If your team genuinely needs extra types, add them to your own commitlint config rather than relying on convention alone, so CI enforces the same list everyone agreed on:
// commitlint.config.js
export default {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
["feat", "fix", "docs", "style", "refactor", "test",
"chore", "perf", "ci", "build", "revert", "deps"],
],
},
};Be aware that release tools only bump versions for feat, fix, and breaking changes unless you configure them otherwise. A custom type is recorded in history but is invisible to your changelog by default.
How do I check commits I already pushed?
Print the subject lines from your history and paste them into the tool one at a time to get a feel for how much of your history is already compliant:
# Last 50 commit subjects, one per line
git log --format=%s -n 50
# Only subjects that already look conventional
git log --format=%s -n 200 | grep -E '^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\([^)]+\))?!?: '