Configuration¶
Commit Check reads its settings from four places. When the same option is set in more than one, the first one listed wins:
- Command-line arguments —
--subject-imperative=true - Environment variables —
CCHK_SUBJECT_IMPERATIVE=true - A configuration file —
cchk.tomlorcommit-check.toml - Built-in defaults
That ordering is what makes the layering useful: the file carries the policy the project agreed on, the environment overrides it for a single CI job, and a flag overrides both for a single run.
Defaults are not "nothing"
Two different things decide whether a rule fires: whether you asked for
that check at all, and what the option defaults to. A check only runs when
its flag is passed — --message never evaluates branch rules — but once a
check is running, these apply with no configuration file present:
| Check | Enforced by default |
|---|---|
--message |
Conventional Commits (CC001), subject lengths of 5–80 characters (CC004, CC005), and an allow-list of ten commit types |
--branch |
Conventional Branch (CC201) and an allow-list of twenty-one branch types |
--author-name / --author-email |
The built-in name and email patterns (CC101, CC102) |
Off until you turn them on: subject capitalization, imperative mood, required body and signoff, and rebase requirements.
The allow_* options are a mix, so read them individually rather than
assuming: allow_commit_types and allow_branch_types are allow-lists
that restrict from the start, while allow_merge_commits,
allow_revert_commits, allow_empty_commits, allow_fixup_commits,
allow_wip_commits and allow_force_push all default to permitting
everything. The Default column in the
rules reference is the full picture.
Where the config file lives¶
The file is TOML, and may be called cchk.toml or commit-check.toml. Commit
Check searches four locations and uses the first that exists:
cchk.tomlcommit-check.toml.github/cchk.toml.github/commit-check.toml
Pass --config to point at one directly and skip the search:
Why .github/
Putting the file in .github/ keeps the repository root uncluttered and
matches where Dependabot and Renovate already keep theirs. Commit Check
treats both locations identically.
Editor autocompletion
The TOML schema is published on SchemaStore,
so VS Code (via
Even Better TOML),
PyCharm and IntelliJ offer completion, validation and inline documentation
for cchk.toml with nothing to configure.
Inheriting a shared config¶
An organization can keep one base policy and have every repository build on it.
Point inherit_from at the shared file; the parent loads first, and anything
set locally overrides it. The key itself is never passed to the validation
engine.
inherit_from = "github:my-org/.github:cchk.toml"
[commit]
subject_max_length = 72 # overrides whatever the parent set
The value can be a GitHub shorthand, a local path or an HTTPS URL:
| Form | Example |
|---|---|
| GitHub, default branch | github:owner/repo:path/to/cchk.toml |
| GitHub, pinned branch | github:owner/repo@main:path/to/cchk.toml |
| Local file | ../../shared/org-cchk.toml |
| HTTPS URL | https://example.com/shared/cchk.toml |
Inheritance fails quietly
If the target is unreachable or the format is not recognized, Commit Check ignores the inheritance and uses the local configuration alone. Plain HTTP URLs are rejected outright. A repository that silently stops inheriting still passes its own checks, so pin the branch when the policy matters.
Pull requests: every commit, or the squash message¶
By default the GitHub App checks every commit of a pull request on its own. A team that squash-merges does not keep those commits: what lands on the base branch is one commit whose message GitHub builds from the pull request. Checking the drafts instead of the result is the reason teams turn commit rules off, so the App can check the result:
With check = "squash" a pull request gets one Commit Check result on its
head commit, and it is the message a squash merge would produce, built the way
GitHub builds it from the repository's merge settings (Settings → General →
Pull Requests):
| Repository setting | Title checked | Body checked |
|---|---|---|
| Default to pull request title | The pull request title | The pull request description, the commit messages, or nothing, per the setting |
| Default message, one commit | That commit's title | That commit's body |
| Default message, several commits | The pull request title | The commit messages, one per bullet |
The pull request number GitHub appends to the title (… (#42)) is part of what
is checked, so a subject-length rule sees the real subject. Editing the title
or description re-runs the check; pushes to the branch are not checked
commit by commit. Author checks run on the head commit, the branch check on
the head branch.
check = "commits" is the default and the behaviour of the CLI, the
pre-commit hook and the Action, which have no pull request to read. The
setting is read by the App only.
Report a rule without enforcing it¶
A rule is normally on or off. warn gives it a third setting: run, report the
finding in full, and never fail the run. It is a top-level key, so it goes
before the first section, and it names a check or its rule ID in any case:
warn = ["branch", "CC003"]
[commit]
subject_imperative = true
[branch]
conventional_branch = true
A warned rule prints the same block as a failure with warning in place of
failed, no rejection banner, and one closing line saying the run is not
failed by it; with --compact it is one [WARN] line. A one-line notice on
stderr names the warned rules, so a hook that exits 0 after red-looking
output is not mistaken for a broken hook. The exit code counts only enforced
rules. In --format json the check's status is warn, the top-level
status stays pass unless an enforced rule failed, and warnings counts
them — see Reading the JSON.
A name that matches no rule is a configuration error that lists the known rules, so a typo cannot leave a rule silently enforced. Rules not listed behave as before.
This is how a team adopts a rule gradually: turn it on as a warning, watch
what it catches for a week, then drop it from warn once the history is
clean. It is also how a shared config carries both
kinds of rule at once — the ones every repository must satisfy, and the ones
it is asking teams to move towards. A repository's own warn list replaces
the shared one, as any inherited key does.
A worked example¶
Every line below that differs from the built-in default is marked, so it is clear what this file is actually changing:
# changed: report the branch rule without enforcing it (nothing is warned by default)
warn = ["branch"]
[commit]
# https://www.conventionalcommits.org
conventional_commits = true
# message_pattern = "" # optional: a custom regex, replacing the above
subject_capitalized = false
subject_imperative = true # changed: off by default
subject_max_length = 80
subject_min_length = 5
# changed: a subset of the default list, which also has perf, build and ci
allow_commit_types = ["feat", "fix", "docs", "style", "refactor", "test", "chore"]
allow_merge_commits = true
allow_revert_commits = true
allow_empty_commits = false # changed: allowed by default
allow_fixup_commits = true
allow_wip_commits = false # changed: allowed by default
require_body = false
require_signed_off_by = false
ai_attribution = "forbid" # changed: "ignore" by default
# ignore_authors = [] # optional: bypass all commit checks for these authors
[push]
allow_force_push = true # set false to block force pushes
[branch]
# https://conventionalbranch.org
conventional_branch = true
# changed: spec types only. The default is a superset — these plus the
# Conventional Commit types, AI agent prefixes and bot prefixes — so setting
# this at all narrows it. Omit the line to accept all of them.
allow_branch_types = ["feature", "bugfix", "hotfix", "release", "chore"]
# allow_branch_names = [] # optional: extra standalone names, e.g. ["develop"]
# require_rebase_target = "main" # optional: no rebase requirement by default
# ignore_authors = [] # optional: as above, for branch checks
allow_* options describe what is permitted
They read backwards from most linters. allow_wip_commits = false is the
setting that rejects WIP commits; leaving it at its default of true
lets them through.
Command-line arguments¶
Every option can be set as a flag, which is what makes a TOML file optional
entirely — useful when the policy lives in .pre-commit-config.yaml instead.
| Type | Form |
|---|---|
| Boolean | --option-name=true / --option-name=false |
| Integer | --option-name=80 |
| List | --option-name=value1,value2,value3 |
| String | --option-name=value |
$ commit-check --message --subject-imperative=false
$ commit-check --message --subject-max-length=72
$ commit-check --message --allow-commit-types=feat,fix,docs
$ commit-check --branch --allow-branch-types=feature,bugfix,hotfix
Used from a hook definition, with no config file anywhere in the repository:
repos:
- repo: https://github.com/commit-check/commit-check
rev: v2.17.0
hooks:
- id: check-message
args:
- --subject-imperative=false
- --subject-max-length=100
- --allow-merge-commits=false
Environment variables¶
Any option can also be set through the environment, which is the practical way
to vary policy per CI job without editing the file. Uppercase the option name,
replace hyphens with underscores, and prefix CCHK_:
$ export CCHK_SUBJECT_MAX_LENGTH=72
$ export CCHK_ALLOW_COMMIT_TYPES=feat,fix,docs,chore
$ CCHK_SUBJECT_MAX_LENGTH=100 commit-check --message
The full mapping between the three forms:
| TOML Config | Environment Variable | CLI Argument |
|---|---|---|
conventional_commits = true |
CCHK_CONVENTIONAL_COMMITS=true |
--conventional-commits=true |
message_pattern = "^PROJ-\\d+: .+" |
CCHK_MESSAGE_PATTERN=^PROJ-\\d+: .+ |
N/A (config file only) |
subject_capitalized = false |
CCHK_SUBJECT_CAPITALIZED=false |
--subject-capitalized=false |
subject_imperative = true |
CCHK_SUBJECT_IMPERATIVE=true |
--subject-imperative=true |
subject_max_length = 80 |
CCHK_SUBJECT_MAX_LENGTH=80 |
--subject-max-length=80 |
subject_min_length = 5 |
CCHK_SUBJECT_MIN_LENGTH=5 |
--subject-min-length=5 |
allow_commit_types = ["feat", "fix"] |
CCHK_ALLOW_COMMIT_TYPES=feat,fix |
--allow-commit-types=feat,fix |
allow_merge_commits = true |
CCHK_ALLOW_MERGE_COMMITS=true |
--allow-merge-commits=true |
allow_revert_commits = true |
CCHK_ALLOW_REVERT_COMMITS=true |
--allow-revert-commits=true |
allow_empty_commits = false |
CCHK_ALLOW_EMPTY_COMMITS=false |
--allow-empty-commits=false |
allow_fixup_commits = true |
CCHK_ALLOW_FIXUP_COMMITS=true |
--allow-fixup-commits=true |
allow_wip_commits = false |
CCHK_ALLOW_WIP_COMMITS=false |
--allow-wip-commits=false |
require_body = false |
CCHK_REQUIRE_BODY=false |
--require-body=false |
require_signed_off_by = false |
CCHK_REQUIRE_SIGNED_OFF_BY=false |
--require-signed-off-by=false |
ignore_authors = ["bot"] |
CCHK_IGNORE_AUTHORS=bot,user |
--ignore-authors=bot,user |
author_email_pattern=^.+@example\.com$ |
CCHK_AUTHOR_EMAIL_PATTERN=^.+@example\.com$ |
--author-email-pattern=^.+@example\.com$ |
author_name_pattern=^.+ .+$ |
CCHK_AUTHOR_NAME_PATTERN=^.+ .+$ |
--author-name-pattern=^.+ .+$ |
conventional_branch = true |
CCHK_CONVENTIONAL_BRANCH=true |
--conventional-branch=true |
allow_branch_types = ["feature"] |
CCHK_ALLOW_BRANCH_TYPES=feature,bugfix |
--allow-branch-types=feature,bugfix |
allow_branch_names = ["develop"] |
CCHK_ALLOW_BRANCH_NAMES=develop,staging |
--allow-branch-names=develop,staging |
require_rebase_target = "main" |
CCHK_REQUIRE_REBASE_TARGET=main |
--require-rebase-target=main |
allow_force_push = true |
CCHK_ALLOW_FORCE_PUSH=true |
--no-force-push (sets allow_force_push to false) |
ai_attribution = "forbid" |
CCHK_AI_ATTRIBUTION=forbid |
--ai-attribution=forbid |
ignore_authors = ["bot"] (in branch section) |
CCHK_BRANCH_IGNORE_AUTHORS=bot,user |
--branch-ignore-authors=bot,user |
regex = "^v\\d+\\.\\d+\\.\\d+$" (in tag section) |
CCHK_TAG_REGEX=^v\\d+\\.\\d+\\.\\d+$ |
--tag-regex=^v\\d+\\.\\d+\\.\\d+$ |
max_size = "5MB" (in files section) |
CCHK_FILES_MAX_SIZE=5MB |
--files-max-size=5MB |
prohibited_patterns = ["*.pem"] (in files section) |
CCHK_FILES_PROHIBITED_PATTERNS=*.pem,.env |
--files-prohibited-patterns=*.pem,.env |
max_path_length = 250 (in files section) |
CCHK_FILES_MAX_PATH_LENGTH=250 |
--files-max-path-length=250 |
Which value wins¶
The four sources layer, so the same option can be set in several at once. Only the highest-priority one takes effect:
$ grep subject_max_length cchk.toml
subject_max_length = 100
$ export CCHK_SUBJECT_MAX_LENGTH=80
$ commit-check --message --subject-max-length=50
The limit applied is 50 — the flag beats the environment, which beats the file. Nothing warns about the values that lost, which is worth remembering when a setting in the file appears to have no effect.
Every option¶
Types are as TOML understands them. A default shown as "" means the option is
unset, which is never the same as the check being off — but it does not mean
the same thing twice, so read the description rather than the cell:
message_patternunset leavesconventional_commitsto generate the pattern. CC001 still runs.author_name_patternunset falls back to the built-in name pattern. CC101 still runs.require_rebase_targetunset is the one case where the check really does not run — there is no branch to compare against.
| Section | Option | Type | Default | Description |
|---|---|---|---|---|
| commit | conventional_commits | bool | true | Enforce Conventional Commits specification. |
| commit | message_pattern | str | "" (no custom pattern) | Custom regex pattern for commit message validation. When set, this pattern replaces the auto-generated Conventional Commits regex entirely, making it possible to enforce custom formats such as JIRA smart commits (e.g., "^PROJ-\\d+: .+"). When message_pattern is set (non-empty) it takes precedence over conventional_commits. |
| commit | subject_capitalized | bool | false | Subject must start with a capital letter. |
| commit | subject_imperative | bool | false | Subject must be in imperative mood. Judged on the first word's form, so a verb no list contains is still accepted — see CC003. |
| commit | subject_max_length | int | 80 | Maximum length of the subject line. |
| commit | subject_min_length | int | 5 | Minimum length of the subject line. |
| commit | allow_commit_types | list[str] | ["feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "build", "ci"] | Allowed commit types when conventional_commits is true. |
| commit | allow_merge_commits | bool | true | Allow merge commits. |
| commit | allow_revert_commits | bool | true | Allow revert commits. |
| commit | allow_empty_commits | bool | true | Allow empty commits. |
| commit | allow_fixup_commits | bool | true | Allow fixup commits (e.g., "fixup! |
| commit | allow_wip_commits | bool | true | Allow work-in-progress commits (e.g., "WIP: |
| commit | require_body | bool | false | Require a body in the commit message. |
| commit | ignore_authors | list[str] | [] (none ignored) | List of commit authors or co-authors (Co-authored-by: lines) to bypass all commit checks. Useful for bots (e.g., "dependabot[bot]", "coderabbitai[bot]"). |
| commit | author_email_pattern | str | ^.+@.+$ |
Custom regex for the author email check. When empty, the built-in default pattern is used. This option only takes effect when the author_email check is enabled (-e / --author-email). |
| commit | author_name_pattern | str | "" (built-in default) | Custom regex for the author name check. When empty, the built-in default pattern is used (it is not disabled). This option only takes effect when the author_name check is enabled (-n / --author-name). |
| commit | require_signed_off_by | bool | false | Require "Signed-off-by" line in the commit message footer. |
| commit | ai_attribution | str | "ignore" | AI attribution policy. "forbid" rejects any commit containing known AI tool signatures (Claude Code, Copilot, Codex, Gemini, Cursor, Devin, Aider, Windsurf, Tabby, and generic AI model patterns). "ignore" disables the check. This feature is a response to the industry-wide discussion on AI disclosure in open source (Linux kernel Assisted-by: trailer, CPython, VS Code, Apache, Fedora policies). |
| branch | conventional_branch | bool | true | Enforce Conventional Branch specification. |
| branch | allow_branch_types | list[str] | ["feature", "bugfix", "hotfix", "release", "chore", "feat", "fix", "build", "ci", "docs", "perf", "refactor", "style", "test", "ai", "claude", "codex", "copilot", "cursor", "dependabot", "renovate"] | Allowed branch types when conventional_branch is true. The default is a superset of the Conventional Branch spec: the spec types (feature, bugfix, hotfix, release, chore) plus the Conventional Commit types (build, ci, docs, perf, refactor, style, test), AI agent prefixes (ai, claude, codex, copilot, cursor) and bot prefixes (dependabot, renovate). For strict spec-only validation, set this option explicitly (e.g. ["feature", "bugfix", "hotfix", "release", "chore"]). |
| branch | allow_branch_names | list[str] | [] (empty list) | Additional standalone branch names allowed when conventional_branch is true (e.g., ["develop", "staging"]). By default, master, main, HEAD, and PR-* are always allowed. |
| branch | require_rebase_target | str | "" (no requirement) | Target branch for rebase requirement. If not set, no rebase validation is performed. |
| push | allow_force_push | bool | true | Allow force pushes. Set to false to block force pushes when used as a pre-push hook or with --no-force-push. |
| branch | ignore_authors | list[str] | [] (none ignored) | List of authors to ignore (i.e., always allow). |
| tag | regex | str | ^v?(?:0\|[1-9]\d*)\.(?:0\|[1-9]\d*)\.(?:0\|[1-9]\d*)(?:-(?:0\|[1-9]\d*\|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0\|[1-9]\d*\|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?$ |
Pattern a tag name must match. The default is the official SemVer pattern with an optional leading v, so both v1.2.3 and 1.2.3 pass. An empty value disables the pattern match. |
| files | max_size | str | "" (disabled) | Largest a committed file may be, in bytes or with a KB/MB/GB suffix (binary units, so 5MB is 5 × 1024²). Empty disables the rule. |
| files | prohibited_patterns | list[str] | [] (empty list) | fnmatch patterns a committed path may not match, e.g. ["*.pem", ".env", "id_rsa*"]. A bare pattern also matches the file name at any depth. Matching is case-sensitive on every platform, like git pathspecs. Empty disables the rule. |
| files | max_path_length | int | 0 (disabled) | Longest a committed file path may be, in characters. 0 disables the rule. |
| pull_request | check | str | "commits" | GitHub App only. "commits" checks every commit of a pull request on its own; "squash" checks the one message a squash merge would land, built from the repository's merge settings. See Pull requests. |