# 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:

```text
CC003 subject-imperative check failed ==> docs: revamped the profile
Commit message should use imperative mood (e.g., 'fix bug' not 'fixed bug', 'add feature' not 'adding feature')
Suggest: Change the first verb to imperative form, e.g., 'fix' instead of 'fixed'/'fixes'/'fixing'
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:

```text
[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](example.md#reading-the-json)):

```text
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
```

## How to read this page

Rule IDs are grouped by what they inspect:

| Range | Category | Inspects |
|---|---|---|
| `CC0xx` | [Commit message](#commit-message-rules) | The subject, body, and trailers of a commit message |
| `CC1xx` | [Author](#author-rules) | The committer's configured name and email |
| `CC2xx` | [Branch](#branch-rules) | The current branch's name and its position relative to a target branch |
| `CC3xx` | [Push and files](#push-rules) | The push operation, and the files a commit touches |
| `CC4xx` | [Tag](#tag-rules) | 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](#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).

## Rule index

### Commit message rules (`CC0xx`) { #commit-message-rules }

Run with `-m` / `--message`.

<div class="rules-index" markdown>

| Code | Name | Message | Check | Default |
|---|---|---|---|---|
| [CC001](#cc001) | `message` | The commit message should follow Conventional Commits | `-m` | ✅ On |
| [CC002](#cc002) | `subject-capitalized` | Subject must start with a capital letter | `-m` | ⚪ Off |
| [CC003](#cc003) | `subject-imperative` | Commit message should use imperative mood | `-m` | ⚪ Off |
| [CC004](#cc004) | `subject-max-length` | Subject must be at most `{max_len}` characters | `-m` | ✅ On |
| [CC005](#cc005) | `subject-min-length` | Subject must be at least `{min_len}` characters | `-m` | ✅ On |
| [CC006](#cc006) | `allow-merge-commits` | Merge commits are not allowed | `-m` | ⚪ Off |
| [CC007](#cc007) | `allow-revert-commits` | Revert commits are not allowed | `-m` | ⚪ Off |
| [CC008](#cc008) | `allow-empty-commits` | Empty commit messages are not allowed | `-m` | ⚪ Off |
| [CC009](#cc009) | `allow-fixup-commits` | Fixup commits are not allowed | `-m` | ⚪ Off |
| [CC010](#cc010) | `allow-wip-commits` | WIP commits are not allowed | `-m` | ⚪ Off |
| [CC011](#cc011) | `require-body` | Commit body is required | `-m` | ⚪ Off |
| [CC012](#cc012) | `require-signed-off-by` | Signed-off-by trailer not found in the latest commit | `-m` | ⚪ Off |
| [CC013](#cc013) | `ai-attribution` | AI attribution policy violation | `-m` | ⚪ Off |
| [CC014](#cc014) | `ai-disclosure` | AI assistance is not disclosed with an accepted trailer | `-m` | ⚪ Off |
| [CC015](#cc015) | `ai-co-author` | An AI tool is credited as a co-author | `-m` | ⚪ Off |
| [CC016](#cc016) | `ai-signoff` | An AI tool signed off the commit | `-m` | ⚪ Off |

</div>

### Author rules (`CC1xx`) { #author-rules }

<div class="rules-index" markdown>

| Code | Name | Message | Check | Default |
|---|---|---|---|---|
| [CC101](#cc101) | `author-name` | The committer name seems invalid | `-n` | ✅ On |
| [CC102](#cc102) | `author-email` | The committer's email seems invalid | `-e` | ✅ On |

</div>

### Branch rules (`CC2xx`) { #branch-rules }

Run with `-b` / `--branch`.

<div class="rules-index" markdown>

| Code | Name | Message | Check | Default |
|---|---|---|---|---|
| [CC201](#cc201) | `branch` | The branch should follow Conventional Branch | `-b` | ✅ On |
| [CC202](#cc202) | `merge-base` | Current branch is not rebased onto target branch | `-b` | ⚪ Off |

</div>

### Push and file rules (`CC3xx`) { #push-rules }

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](#cc303).

<div class="rules-index" markdown>

| Code | Name | Message | Check | Default |
|---|---|---|---|---|
| [CC301](#cc301) | `no-force-push` | Force push is not allowed | `--no-force-push` | ⚪ Off |
| [CC302](#cc302) | `file-size` | File exceeds the maximum size of `{max_size}` | `-f` | ⚪ Off |
| [CC303](#cc303) | `file-pattern` | File path matches a prohibited pattern | `-f` | ⚪ Off |
| [CC304](#cc304) | `path-length` | File path exceeds `{max_len}` characters | `-f` | ⚪ Off |

</div>

### Tag rules (`CC4xx`) { #tag-rules }

Run with `-t` / `--tag`.

<div class="rules-index" markdown>

| Code | Name | Message | Check | Default |
|---|---|---|---|---|
| [CC401](#cc401) | `tag` | The tag name does not match the required pattern | `-t` | ✅ On |

</div>

## Commit message rules

### message (CC001) { #cc001 }

**What it does**

Checks that the commit message subject follows the
[Conventional Commits](https://www.conventionalcommits.org) specification:
`<type>(<scope>)!: <description>`.

A subject that a merge tool writes, such as git's `Merge branch 'main' into
topic` or GitHub's `Merge pull request #12 from …`, is accepted as it is;
whether merge commits are allowed at all is [CC006](#cc006)'s call. A subject
someone typed that merely starts with `Merge `, such as `Merge the parser
rewrite`, is judged like any other.

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

```text
updated the parser
```

Use instead:

```text
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) { #cc002 }

**What it does**

Checks that the description in the subject line starts with a capital letter.

The description is what follows a `<type>(<scope>)!:` prefix, and the prefix
is skipped only when Conventional Commits is on and the type is one of
`allow_commit_types`. Otherwise the subject is read from its first word, so
`Added: support for X` is judged on `Added`.

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

```text
fix: handle empty input
```

Use instead:

```text
fix: Handle empty input
```

**Options**

* `commit.subject_capitalized` — set to `true` to enable this rule.

### subject-imperative (CC003) { #cc003 }

**What it does**

Checks that the first word of the description is in the imperative mood —
`fix`, not `fixed`, `fixes`, or `fixing`.

The description is what follows a `<type>(<scope>)!:` prefix, and the prefix
is skipped only when Conventional Commits is on and the type is one of
`allow_commit_types`. Otherwise the subject is read from its first word, so
`Added: support for X` is judged on `Added`.

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

```text
fix: fixed a crash when the config file is empty
```

Use instead:

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

### subject-max-length (CC004) { #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**

```text
fix: handle an empty config file and also fix the unrelated crash in the branch parser that happens on Windows
```

Use instead:

```text
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) { #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**

```text
fix
```

Use instead:

```text
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) { #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**

```bash
git pull
```

Use instead:

```bash
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) { #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**

```text
Revert "feat: add caching layer"
```

Use instead:

```text
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) { #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, and
once it is pushed, adding a message means rewriting history.

**Options**

* `commit.allow_empty_commits` — set to `false` to enable this rule.

### allow-fixup-commits (CC009) { #cc009 }

**What it does**

Rejects `fixup!`, `squash!` and `amend!` commits, the ones `git commit
--fixup`, `--squash` and `--fixup=amend:` write.

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

```text
fixup! feat: add the caching layer
```

Use instead:

```bash
git rebase -i --autosquash main
```

**Options**

* `commit.allow_fixup_commits` — set to `false` to enable this rule.

### allow-wip-commits (CC010) { #cc010 }

**What it does**

Rejects work-in-progress commits — subjects beginning with the word `WIP` or
with `[WIP]`, in any case and whatever follows. Words that merely start with
the letters, such as `wipe`, pass, and so does a body that mentions 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**

```text
WIP: caching
```

Use instead:

```bash
# 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) { #cc011 }

**What it does**

Requires a non-empty body after the subject line.

A message file is judged as the message git will store: comment lines, and
everything from the `git commit -v` scissors line on, do not count. So a
subject-only commit written in the editor fails, although git's comment block
follows the subject when the hook runs.

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

```text
fix: cap the retry backoff at 30s
```

Use instead:

```text
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) { #cc012 }

**What it does**

Requires a `Signed-off-by:` trailer in the commit message: a line of its own
that names a person and an `<address>`, as `git commit --signoff` writes it.
The same text inside the subject or a sentence of the body does not count.

**Why is this bad?**

Projects that use the
[Developer Certificate of Origin](https://developercertificate.org/) — 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**

```bash
git commit -m "fix: handle an empty config file"
```

Use instead:

```bash
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) { #cc013 }

**What it does**

Rejects a commit whose message carries any AI attribution: a co-author or
sign-off line naming Claude Code, Copilot, Codex, Gemini, Cursor, Devin,
Aider, Windsurf, Tabby, Amp, OpenHands, Factory Droid or Jules, a disclosure
trailer such as `Assisted-by:` or `Generated-by:`, a vendor's own mark such
as `🤖 Generated with Claude Code`, Amp's `Amp-Thread-ID:` trailer, the
`Agent-Logs-Url:` trailer the Copilot coding agent adds or Cursor's
`Made-with: Cursor`, and generic AI model names. Copilot is also recognised by its account's
`N+Copilot@users.noreply.github.com` address, so the coding agent's
`copilot-swe-agent[bot]` co-author line counts under any display name.

**Why is this bad?**

Some projects want their history to carry no AI attribution at all — the
[Kubernetes contributor guide](https://www.kubernetes.dev/docs/guide/pull-requests/)
takes that position, asking for disclosure in the pull request instead. This
rule exists for projects that have made that decision and want it enforced
mechanically rather than relitigated in every review.

**Example**

```text
CC013 ai-attribution check failed ==> Claude Code
AI attribution is forbidden in this project — detected: Claude Code
Suggest: This project does not accept AI attribution in commit messages. Remove the AI trailer lines and re-commit.
```

**Options**

* `commit.ai_attribution` — `"forbid"` enables this rule. `"ignore"` (the
  default) disables it; `"disclose"` replaces it with
  [CC014](#cc014)–[CC016](#cc016).

### ai-disclosure (CC014) { #cc014 }

**What it does**

Under `ai_attribution = "disclose"`, requires that a commit showing signs of
AI assistance discloses it with one of the project's trailers — by default
`Assisted-by:` or `Generated-by:`. A vendor's co-author line, an AI sign-off,
a `🤖 Generated with` mark, or a disclosure written with some other trailer
all count as signs; none of them counts as the disclosure.

A trailer that is there but says nothing — no value, or a value that does not
match `ai_disclosure_pattern` — fails too.

**Why is this bad?**

A project that allows AI assistance and asks for it to be disclosed has an
honour-system rule until something checks it.
[Fedora](https://docs.fedoraproject.org/en-US/council/policy/ai-policy/) and
FluxCD ask for `Assisted-by:`, as the
[Linux kernel](https://docs.kernel.org/process/coding-assistants.html) did
until its maintainers decided in October 2026 to drop the tag; the
[Apache Software Foundation](https://www.apache.org/legal/generative-tooling.html)
recommends `Generated-by:`. The tools, meanwhile, stamp their own format on
the way out, so a contributor following the policy and one ignoring it produce
commits that look much the same.

**Example**

```text
CC014 ai-disclosure check failed ==> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AI assistance is not disclosed with Assisted-by or Generated-by — detected: Claude Code
Suggest: Disclose the tool with "Assisted-by: Claude Opus 5"
```

The suggestion keeps the name the tool gave itself, and `--format json`
carries the whole corrected message in `fix`.

**Options**

* `commit.ai_attribution` — set to `"disclose"` to enable this rule.
* `commit.ai_disclosure_trailers` — the trailers that count as a disclosure.
  The first one is what a correction is written with.
* `commit.ai_disclosure_pattern` — a regex the disclosure's value must match,
  for a project that wants a particular format.

### ai-co-author (CC015) { #cc015 }

**What it does**

Under `ai_attribution = "disclose"`, rejects a commit that credits an AI tool
as a person: a `Co-authored-by:` or `Co-developed-by:` trailer naming a
recognised tool.

**Why is this bad?**

A co-author line is a claim about who wrote the change, and it travels: GitHub
counts co-authors as contributors, and a squash merge carries the line into
the default branch. Projects that welcome AI assistance still commonly draw
the line here — [pip](https://github.com/pypa/pip/blob/main/AI_POLICY.md),
`attrs` and Requests all say no AI in `Co-authored-by:` — because disclosure
and authorship are different claims.

**Example**

```text
CC015 ai-co-author check failed ==> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An AI tool is credited as a co-author: Claude Code
Suggest: Use "Assisted-by: Claude Opus 5" in place of the co-author line
```

**Options**

* `commit.ai_attribution` — set to `"disclose"` to enable this rule.
* `commit.ai_disclosure_trailers` — listing `Co-authored-by` here says the
  project accepts the tool as a co-author, and this rule then passes.

### ai-signoff (CC016) { #cc016 }

**What it does**

Under `ai_attribution = "disclose"`, rejects a commit whose `Signed-off-by:`
trailer names an AI tool.

**Why is this bad?**

A sign-off is a legal certification — the
[Developer Certificate of Origin](https://developercertificate.org/) — and
only a person can make it. The Linux kernel's guidance says so outright: AI
agents must not add `Signed-off-by` tags, because they cannot certify the
DCO. No trailer list may name `Signed-off-by`, so this rule cannot be turned
off by accepting it as a disclosure.

**Example**

```text
CC016 ai-signoff check failed ==> Signed-off-by: Claude <noreply@anthropic.com>
An AI tool signed off the commit: Claude Code
Suggest: Use "Assisted-by: Claude" in place of the AI sign-off, then sign off yourself (git commit --signoff)
```

**Options**

* `commit.ai_attribution` — set to `"disclose"` to enable this rule.

## Author rules

### author-name (CC101) { #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**

```bash
git config user.name 12
```

Use instead:

```bash
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 a name that starts with a letter and goes on
    with letters, digits, `_`, spaces and `, . ' -`, so account names such as
    `root`, `ubuntu` or `ec2-user` still pass it; a name that starts with a
    digit or holds other symbols (`@`, `/`, ...) is rejected. Set
    `author_name_pattern` if you need something stricter.

### author-email (CC102) { #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:

```bash
git config user.email you@gmail.com
```

Use instead:

```bash
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.

To reject an address instead of requiring one, use a negative lookahead. This
refuses GitHub's no-reply addresses (`jane@users.noreply.github.com` and
`12345+jane@users.noreply.github.com`), which cannot receive mail and can
lose their link to the author's account after a GitHub username change:

```toml title="cchk.toml"
[commit]
author_email_pattern = '(?i)^(?!.*@users\.noreply\.github\.com$).+@.+$'
```

!!! 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) { #cc201 }

**What it does**

Checks that the current branch name follows the
[Conventional Branch](https://conventionalbranch.org/) specification:
`<type>/<description>`.

`main`, `master`, `develop`, `HEAD`, `PR-*`, and the `gh-readonly-queue/*`
branches GitHub's merge queue creates are always accepted.

By default only the type is checked, and any description after the `/` is
accepted. With `require_description_grammar` the description has to follow
the specification's grammar too: lowercase letters and digits, words joined
by single hyphens, dots only inside a word, as in `release/v1.2.0`.

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

```text
my-fix
johns-branch-2
```

Use instead:

```text
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's types (`feature`, `bugfix`,
  `hotfix`, `release`, `chore`, their aliases `feat` and `fix`, and the AI
  agent prefixes `ai`, `claude`, `codex`, `copilot` and `cursor`), plus the
  other Conventional Commit types, `agents` (the prefix VS Code's Agents
  window uses) and the bot prefixes `dependabot` and `renovate`. Set it
  explicitly for strict spec-only validation.
* `branch.require_description_grammar` — set to `true` to hold the
  description to the specification's grammar, so `feature/Add_Login`,
  `feature/new--login` and `feature/a/b` are rejected. Off by default,
  because bot branches such as `dependabot/npm_and_yarn/lodash-4.17.21` do
  not follow it.
* `branch.allow_branch_names` — additional standalone names to accept, such as
  `["staging", "production"]`.
* `branch.ignore_authors` — bypass the check for specific authors.

To check branches exactly as the specification does, set the spec's types and
the description grammar. Its trunk branches, `main`, `master` and `develop`,
are accepted already:

```toml title="cchk.toml"
[branch]
conventional_branch = true
allow_branch_types = ["feature", "feat", "bugfix", "fix", "hotfix", "release", "chore", "ai", "claude", "codex", "copilot", "cursor"]
require_description_grammar = true
```

With that, `commit-check --branch` agrees with every case of the
specification's [conformance suite](https://github.com/conventional-branch/conventional-branch/blob/main/tests/fixtures.json)
that git can create as a branch.

### merge-base (CC202) { #cc202 }

**What it does**

Checks that the current branch is rebased onto a target branch.

When the target matches no branch, neither locally nor under `origin/`, the
check is a **skip**, not a pass, and the JSON `reason` says
`rebase target 'main' not found`. A shallow CI clone without the target branch
is the usual cause; fetch the branch before the check.

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

```bash
# branch was cut from main three weeks ago
git push
```

Use instead:

```bash
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 and file rules

### no-force-push (CC301) { #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**

```bash
git push --force
```

Use instead:

```bash
# 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.

### file-size (CC302) { #cc302 }

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

```toml title="cchk.toml"
[files]
max_size = "5MB"
```

**Options**

* `files.max_size` — largest a committed file may be, in bytes or with a
  `KB`/`MB`/`GB` suffix. Empty (the default) disables this rule.

### file-pattern (CC303) { #cc303 }

**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](https://github.com/gitleaks/gitleaks) — each catches
what the other cannot.

**Example**

```toml title="cchk.toml"
[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.

### path-length (CC304) { #cc304 }

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

```toml title="cchk.toml"
[files]
max_path_length = 250
```

**Options**

* `files.max_path_length` — longest a committed file path may be, in
  characters. `0` (the default) disables this rule.

## Tag rules

### tag (CC401) { #cc401 }

**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](https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string)
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.

As a `pre-push` hook it checks the tags the push creates, under the name the
remote will give them: `git push origin v1.0.0:refs/tags/release-1` is checked
as `release-1`. Deleting a tag is not checked.

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

```bash
git tag v1.2                  # not SemVer: no patch version
git tag release-1.2.3         # not SemVer: prefixed
```

Use instead:

```bash
git tag v1.2.3
git tag v1.2.3-rc.1
```

**Options**

* `tag.regex` — the pattern a tag name must match. An empty value disables the
  pattern match.
