Every check that can report a failure has a stable rule ID. Rule IDs never change once released, so they are safe to reference in commit messages, code review comments, issue templates, and tooling.
Rule IDs appear in commit-check's output and in --format json results:
CC003 subject-imperative check failed ==> docs: revamped the profile
Commit message should use imperative mood (e.g., 'fix bug' not 'fixed bug')
Suggest: Change the first verb to imperative form, e.g., 'fix' instead of 'fixed'
Docs: https://commit-check.com/rules/#cc003
--compact prints one line per failure, keeping the rule ID and dropping the
explanation, suggestion, and documentation link:
[FAIL] CC003 subject_imperative: docs: revamped the profile
When the correction is mechanical, the suggestion names it instead of
repeating the rule — and --format json carries the corrected value in a
fix field (see Reading the JSON):
CC001 message check failed ==> Fix: add x
The commit message should follow Conventional Commits. See https://www.conventionalcommits.org
Suggest: Use "fix: add x"
Docs: https://commit-check.com/rules/#cc001
Rule IDs are grouped by what they inspect:
| Range | Category | Inspects |
|---|---|---|
CC0xx |
Commit message | The subject, body, and trailers of a commit message |
CC1xx |
Author | The committer's configured name and email |
CC2xx |
Branch | The current branch's name and its position relative to a target branch |
CC3xx |
Push and files | The push operation, and the files a commit touches |
CC4xx |
Tag | The names of the tags pointing at the commit |
Two things determine whether a rule runs:
The check you select. commit-check only evaluates the checks you ask for on
the command line. commit-check --message never reports a branch rule. The
Check column in the tables below shows which flag activates each rule.
Its configuration. Within a selected check, the Default column shows whether the rule is active with no configuration at all:
| Default | Meaning |
|---|---|
| ✅ On | Enforced out of the box. Disable it through the listed option. |
| ⚪ Off | Not enforced until you opt in through the listed option. |
Rules that are off by default are not lesser rules — they encode conventions that are right for some projects and wrong for others. Turning on CC002 makes sense for a project that capitalizes subjects, and is actively harmful for one that does not.
!!! tip
Every option named below is documented in full — with its type, default, and
the matching environment variable and CLI flag — in
[configuration](configuration.md).
Run with -m / --message.
| Code | Name | Message | Check | Default |
|---|---|---|---|---|
| CC001 | message |
The commit message should follow Conventional Commits | -m |
✅ On |
| CC002 | subject-capitalized |
Subject must start with a capital letter | -m |
⚪ Off |
| CC003 | subject-imperative |
Commit message should use imperative mood | -m |
⚪ Off |
| CC004 | subject-max-length |
Subject must be at most {max_len} characters |
-m |
✅ On |
| CC005 | subject-min-length |
Subject must be at least {min_len} characters |
-m |
✅ On |
| CC006 | allow-merge-commits |
Merge commits are not allowed | -m |
⚪ Off |
| CC007 | allow-revert-commits |
Revert commits are not allowed | -m |
⚪ Off |
| CC008 | allow-empty-commits |
Empty commit messages are not allowed | -m |
⚪ Off |
| CC009 | allow-fixup-commits |
Fixup commits are not allowed | -m |
⚪ Off |
| CC010 | allow-wip-commits |
WIP commits are not allowed | -m |
⚪ Off |
| CC011 | require-body |
Commit body is required | -m |
⚪ Off |
| CC012 | require-signed-off-by |
Signed-off-by not found in latest commit | -m |
⚪ Off |
| CC013 | ai-attribution |
AI attribution policy violation | -m |
⚪ Off |
| Code | Name | Message | Check | Default |
|---|---|---|---|---|
| CC101 | author-name |
The committer name seems invalid | -n |
✅ On |
| CC102 | author-email |
The committer's email seems invalid | -e |
✅ On |
Run with -b / --branch.
| Code | Name | Message | Check | Default |
|---|---|---|---|---|
| CC201 | branch |
The branch should follow Conventional Branch | -b |
✅ On |
| CC202 | merge-base |
Current branch is not rebased onto target branch | -b |
⚪ Off |
The file rules run with -f / --files. They read only the paths and sizes
recorded in a commit — never file contents, which is a
different tool's job.
| Code | Name | Message | Check | Default |
|---|---|---|---|---|
| CC301 | no-force-push |
Force push is not allowed | --no-force-push |
⚪ Off |
| CC302 | file-size |
File exceeds the maximum size of {max_size} |
-f |
⚪ Off |
| CC303 | file-pattern |
File path matches a prohibited pattern | -f |
⚪ Off |
| CC304 | path-length |
File path exceeds {max_len} characters |
-f |
⚪ Off |
Run with -t / --tag.
| Code | Name | Message | Check | Default |
|---|---|---|---|---|
| CC401 | tag |
The tag name does not match the required pattern | -t |
✅ On |
What it does
Checks that the commit message subject follows the
Conventional Commits specification:
<type>(<scope>)!: <description>.
Why is this bad?
A free-form subject can only be read by a human. A structured one can be read by
tooling: release-drafting can group changes by type, semantic versioning can
infer whether a release is a patch, minor, or major, and git log becomes
filterable by area of the codebase. Once a fraction of the history is
unstructured, every consumer of that history needs a fallback path.
Example
updated the parser
Use instead:
fix(parser): handle empty input
Options
commit.conventional_commits— set tofalseto disable this rule.commit.allow_commit_types— the accepted<type>values.commit.message_pattern— a custom regex that replaces the generated Conventional Commits pattern entirely, for formats such as JIRA smart commits ("^PROJ-\\d+: .+").
What it does
Checks that the description in the subject line starts with a capital letter.
Why is this bad?
Nothing is inherently wrong with either casing — but mixing them is. A history
where half the subjects read fix: handle empty input and the other half read
fix: Handle empty input looks careless in git log --oneline, and gives
reviewers a pointless thing to comment on. This rule picks the capitalized
convention and enforces it.
Leave it off if your project deliberately uses lowercase descriptions, which is the more common convention among projects that follow Conventional Commits.
Example
fix: handle empty input
Use instead:
fix: Handle empty input
Options
commit.subject_capitalized— set totrueto enable this rule.
What it does
Checks that the first word of the description is in the imperative mood —
fix, not fixed, fixes, or fixing.
Why is this bad?
This is Git's own convention: a subject should complete the sentence "If
applied, this commit will ___". If applied, this commit will fixed a crash
does not read as English. Beyond grammar, the imperative form is the shortest of
the three, which matters on a line that tooling truncates around 50 characters.
Example
fix: fixed a crash when the config file is empty
Use instead:
fix: handle an empty config file
What it does not do
The rule reads the form of the first word, not a vocabulary, so it does not
need to recognise your verb. Anything that is not a past tense, a gerund or a
third-person singular is accepted — including verbs no word list contains, and
subjects that lead with a noun (fix: parser improvements), which have no mood
to get wrong.
The trade is that a word's form does not always tell you how it is being used.
fix: spelling in the docs is reported: spelling is a noun there, but it
wears the -ing of a gerund, and a rule that reads the ending cannot tell the
two apart.
Options
commit.subject_imperative— set totrueto enable this rule.
What it does
Checks that the subject line is at most a configured number of characters.
Why is this bad?
Long subjects get truncated by the tools that display them —
git log --oneline, git shortlog, GitHub's commit list, and most Git
GUIs all cut off somewhere between 50 and 72 columns. A subject that carries its
meaning past that point loses it exactly where people skim. Detail belongs in
the body, which nothing truncates.
Example
fix: handle an empty config file and also fix the unrelated crash in the branch parser that happens on Windows
Use instead:
fix: handle an empty config file
Also fixes the branch parser crash on Windows, which shared the
same root cause.
Options
commit.subject_max_length— the limit, in characters. Defaults to80.50and72are the other conventional choices.
What it does
Checks that the subject line is at least a configured number of characters.
Why is this bad?
Subjects like fix, wip, or . describe nothing. They are invisible in
a blame view and useless in a bisect session, and they are almost always the
result of a hurried commit rather than a deliberate one.
Example
fix: bug
Use instead:
fix: reject config files with a null inherit_from
Options
commit.subject_min_length— the minimum, in characters. Defaults to5.
What it does
Rejects merge commits — the Merge branch '...' commits that git pull
creates.
Why is this bad?
Merge commits created by git pull carry no information: they record that
someone synced, not that anything was decided. On a busy repository they can
outnumber real commits, which makes git log unreadable, adds branches for
git bisect to walk, and breaks the assumption behind
git log --first-parent. Projects that want a linear history rebase instead.
Example
git pullUse instead:
git pull --rebase
# or make it the default
git config --global pull.rebase trueOptions
commit.allow_merge_commits— set tofalseto enable this rule.
What it does
Rejects the Revert "..." commits that git revert generates.
Why is this bad?
A generated revert subject describes the mechanics of the change and nothing
about the reason for it. Six months later, Revert "feat: add caching layer"
answers "what happened" but not the only question that matters: why the feature
was backed out, and whether it is safe to try again.
Example
Revert "feat: add caching layer"
Use instead:
fix: remove the caching layer
The cache served stale permissions after a role change (#412).
Reverts 4a1c9f2; re-land once invalidation is keyed on role version.
Options
commit.allow_revert_commits— set tofalseto enable this rule.
What it does
Rejects commits with an empty message.
Why is this bad?
A commit with no subject cannot be searched for, summarised, or reviewed. It is a gap in the history that nobody can fill in later.
Options
commit.allow_empty_commits— set tofalseto enable this rule.
What it does
Rejects fixup! and squash! commits.
Why is this bad?
These commits exist to be consumed by git rebase --autosquash before a
branch is merged. One that survives to the target branch means the autosquash
was forgotten — leaving behind a commit that, by construction, does not stand on
its own.
Example
fixup! feat: add the caching layer
Use instead:
git rebase -i --autosquash mainOptions
commit.allow_fixup_commits— set tofalseto enable this rule.
What it does
Rejects work-in-progress commits — subjects beginning with WIP.
Why is this bad?
A WIP commit is an explicit statement that the change is not finished. That is useful on a local branch and wrong on a shared one, where every commit is something another developer may bisect through or build on.
Example
WIP: caching
Use instead:
# keep the work, drop the marker
git commit --amend -m "feat: add a caching layer for role lookups"Options
commit.allow_wip_commits— set tofalseto enable this rule.
What it does
Requires a non-empty body after the subject line.
Why is this bad?
The subject says what changed; the diff already says that too. The body says why — the constraint, the bug report, the rejected alternative. That reasoning is the one thing that cannot be recovered from the code later, and it is exactly what the next person to touch the change needs.
Example
fix: cap the retry backoff at 30s
Use instead:
fix: cap the retry backoff at 30s
The unbounded exponential backoff reached 45 minutes during the
incident on 2026-05-11, long after the upstream had recovered.
Options
commit.require_body— set totrueto enable this rule.
What it does
Requires a Signed-off-by: trailer in the commit message.
Why is this bad?
Projects that use the Developer Certificate of Origin — the Linux kernel, and much of the CNCF — treat that trailer as the contributor's statement that they have the right to submit the code. A commit without it cannot be merged, so catching it locally saves a round trip through CI.
Example
git commit -m "fix: handle an empty config file"Use instead:
git commit --signoff -m "fix: handle an empty config file"
# or fix the commit you already made
git commit --amend --signoffOptions
commit.require_signed_off_by— set totrueto enable this rule.
What it does
Rejects commits carrying the signatures that AI coding tools add to commit messages — trailers naming Claude Code, Copilot, Codex, Gemini, Cursor, Devin, Aider, Windsurf, Tabby, and generic AI model patterns.
Why is this bad?
Whether AI-assisted commits are acceptable is a policy question, and projects
have landed on different answers: the Linux kernel added an Assisted-by:
trailer, while others disallow the practice outright. This rule exists for
projects that have made that decision and want it enforced mechanically rather
than relitigated in every code review.
It is off by default, and the default policy is "ignore". Enable it only if
your project has a stated position.
Options
commit.ai_attribution—"forbid"enables this rule,"ignore"(the default) disables it.
What it does
Checks the committer's configured name against a pattern. The built-in pattern
accepts letters (including accented Latin characters), spaces, and
, . ' -, and always allows [bot] accounts.
Why is this bad?
When user.name is unset, Git falls back to the machine's account name.
Histories built in CI containers and on fresh VMs fill up with commits by
machine accounts — authorship that cannot be traced back to a person, which
matters for both code archaeology and compliance.
Example
git config user.name ec2-userUse instead:
git config --global user.name "Your Name"Options
commit.author_name_pattern— a custom regex replacing the built-in pattern. For example,"^.+ .+$"to require a full name.
!!! note
The built-in pattern accepts any name made of letters, spaces, and
`, . ' -`, so plain account names such as `root` or `ubuntu` still
pass it — only names containing digits or other symbols are rejected. Set
`author_name_pattern` if you need something stricter.
What it does
Checks the committer's configured email against a pattern. The built-in pattern
is ^.+@.+$, which only requires an @ with something on either side.
Why is this bad?
An address with no @ is not routable, so it breaks the link between a commit
and its author: forges cannot attribute the commit to an account, and
mailmap-based tooling cannot merge identities.
The built-in pattern is deliberately permissive — it is a sanity check, not a policy. Its real value comes from replacing it, which is how organisations require contributions to come from a corporate address.
Example
With author_email_pattern = "^.+@example\\.com$" configured:
git config user.email you@gmail.comUse instead:
git config --global user.email you@example.comOptions
commit.author_email_pattern— the regex to match against. Defaults to^.+@.+$; set something like"^.+@example\\.com$"to require a company domain.
!!! note
Because the built-in pattern only looks for an `@`, local and placeholder
addresses such as `root@localhost` pass it. Set `author_email_pattern`
if you need to reject those.
What it does
Checks that the current branch name follows the
Conventional Branch specification:
<type>/<description>.
master, main, HEAD, and PR-* are always accepted.
Why is this bad?
A predictable prefix is something automation can act on: CI can skip expensive
jobs for docs/ branches, deployment workflows can key off release/, and
branch protection rules can be written per type. It also makes a list of a
hundred open branches scannable, which an unstructured list never is.
Example
my-fix
johns-branch-2
Use instead:
fix/empty-config-crash
feature/role-caching
Options
branch.conventional_branch— set tofalseto disable this rule.branch.allow_branch_types— the accepted<type>values. The default is a superset of the specification: the spec types plus the Conventional Commit types, AI agent prefixes (ai,claude,codex,copilot,cursor), and bot prefixes (dependabot,renovate). Set it explicitly for strict spec-only validation.branch.allow_branch_names— additional standalone names to accept, such as["develop", "staging"].branch.ignore_authors— bypass the check for specific authors.
What it does
Checks that the current branch is rebased onto a target branch.
Why is this bad?
A branch that has fallen behind is tested against code that no longer exists on the target. CI passing on it says little about whether it will pass after merging, and the failures it hides — a renamed function, a changed migration — surface on the target branch instead of the pull request.
Example
# branch was cut from main three weeks ago
git pushUse instead:
git fetch origin
git rebase origin/main
git push --force-with-leaseOptions
branch.require_rebase_target— the target branch, for example"main". Unset by default, meaning no rebase requirement.
What it does
Blocks force pushes. Run it as a pre-push hook, where it reads the push
details from stdin, or with --no-force-push, where it compares the current
branch against its upstream.
Why is this bad?
A force push to a shared branch rewrites history that other people have already based work on. Their next pull produces conflicts against commits that no longer exist, and any commit pushed between their fetch and the force push is silently dropped. On a personal branch this is a routine part of rebasing; on a shared one it is a data-loss event.
Example
git push --forceUse instead:
# on a shared branch, add a commit rather than rewriting
git revert <sha>
# on your own branch, at least refuse to clobber someone else's work
git push --force-with-leaseOptions
push.allow_force_push— set tofalseto enable this rule.
What it does
Rejects a commit that adds or updates a file larger than files.max_size. The
size read is the one recorded in the commit, not whatever the working tree
holds now, so the verdict is the same wherever the check runs.
Why is this bad?
Git stores every version of a file forever. A large binary committed once stays in the pack for the life of the repository, and every clone downloads it — including the clones of people who will never open it. Removing the file later does not shrink history; only a rewrite does, and by then everyone has already pulled it. Large assets belong in Git LFS or an artifact store.
Example
[files]
max_size = "5MB"Options
files.max_size— largest a committed file may be, in bytes or with aKB/MB/GBsuffix. Empty (the default) disables this rule.
What it does
Rejects a commit that touches a path matching any files.prohibited_patterns
entry. Patterns are fnmatch globs, matched against the full path and against
the file name alone, so a bare *.pem catches the file at any depth. Matching
is case-sensitive on every platform, like git pathspecs.
Why is this bad?
Some files are never meant to be in a repository: private keys, .env files,
credential dumps, build output. A path rule is the cheapest possible guard —
it costs one glob comparison and needs no knowledge of what is inside the file.
This organization once shipped a GitHub App private key as a committed .pem;
one line of configuration would have stopped that commit.
A path rule is not secret scanning. It cannot see a token pasted into a .txt
file, and it happily allows an empty secrets.pem. Pair it with a content
scanner such as gitleaks — each catches
what the other cannot.
Example
[files]
prohibited_patterns = ["*.pem", "*.key", ".env", "id_rsa*"]Options
files.prohibited_patterns— fnmatch patterns a committed path may not match. Empty (the default) disables this rule.
What it does
Rejects a commit whose file paths exceed files.max_path_length characters.
Why is this bad?
Path length limits are a portability problem, not a style preference. Windows caps a path at 260 characters unless long paths are explicitly enabled, so a deeply nested file added on Linux can make the repository impossible to check out on a teammate's machine — and the failure surfaces at clone time, far from the commit that caused it.
Example
[files]
max_path_length = 250Options
files.max_path_length— longest a committed file path may be, in characters.0(the default) disables this rule.
What it does
Checks the names of the tags pointing at the commit under test against
tag.regex. The default is the
official SemVer pattern
with an optional leading v, so v1.2.3 and 1.2.3 both pass while v1.2
and release-1.2.3 do not.
A commit with no tag is a skip, not a failure: the rule governs how tags are named, and the absence of one is not a naming violation.
Why is this bad?
Tags are the public interface of a release. Package managers, changelog
generators, and pre-commit's own rev: all resolve a version string to a
tag, and every one of them assumes a shape. A single release-2.0 among a
history of v1.x.y tags breaks version sorting silently — the tag still
exists, it just stops being found.
Example
git tag v1.2 # not SemVer: no patch version
git tag release-1.2.3 # not SemVer: prefixedUse instead:
git tag v1.2.3
git tag v1.2.3-rc.1Options
tag.regex— the pattern a tag name must match. An empty value disables the pattern match.