Skip to content

Rules

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://docs.commit-check.com/rules.html#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

How to read this page

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 The push operation itself

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.

Rule index

Commit message rules (CC0xx)

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

Author rules (CC1xx)

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

Branch rules (CC2xx)

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

Push rules (CC3xx)

Code Name Message Check Default
CC301 no-force-push Force push is not allowed --no-force-push ⚪ Off

Commit message rules

message (CC001)

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 to false to 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+: .+").

subject-capitalized (CC002)

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 to true to enable this rule.

subject-imperative (CC003)

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

Options

  • commit.subject_imperative — set to true to enable this rule.

The list of recognised non-imperative verb forms lives in imperatives.py.

subject-max-length (CC004)

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 to 80. 50 and 72 are the other conventional choices.

subject-min-length (CC005)

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 to 5.

allow-merge-commits (CC006)

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 pull

Use instead:

git pull --rebase

# or make it the default
git config --global pull.rebase true

Options

  • commit.allow_merge_commits — set to false to enable this rule.

allow-revert-commits (CC007)

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 to false to enable this rule.

allow-empty-commits (CC008)

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 to false to enable this rule.

allow-fixup-commits (CC009)

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 main

Options

  • commit.allow_fixup_commits — set to false to enable this rule.

allow-wip-commits (CC010)

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 to false to enable this rule.

require-body (CC011)

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 to true to enable this rule.

require-signed-off-by (CC012)

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 --signoff

Options

  • commit.require_signed_off_by — set to true to enable this rule.

ai-attribution (CC013)

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.

Author rules

author-name (CC101)

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-user

Use 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.

author-email (CC102)

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.com

Use instead:

git config --global user.email you@example.com

Options

  • 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.

Branch rules

branch (CC201)

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 to false to 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.

merge-base (CC202)

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 push

Use instead:

git fetch origin
git rebase origin/main
git push --force-with-lease

Options

  • branch.require_rebase_target — the target branch, for example "main". Unset by default, meaning no rebase requirement.

Push rules

no-force-push (CC301)

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 --force

Use 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-lease

Options

  • push.allow_force_push — set to false to enable this rule.