diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index c2c6b27..7f2b865 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -25,15 +25,20 @@ jobs:
with:
python-version: '3.x'
- # Installed from main rather than PyPI: the site documents rule IDs that
- # are merged but not yet released, so the published package would report
- # a spurious mismatch. Switch this to the released package once a version
- # carrying them is on PyPI.
- - name: Install commit-check from main
+ # The released package, deliberately unpinned. This site documents the
+ # version a reader can actually install, so the check is only meaningful
+ # against that — pinning would let the two drift apart silently until
+ # somebody bumped the pin.
+ #
+ # This used to install from main, because the rule IDs the site documents
+ # were merged but unreleased and PyPI would have reported a spurious
+ # mismatch. v2.13.0 shipped them, so the exception is over. If a future
+ # release is documented here ahead of time, point this back at main for
+ # as long as that is true, and no longer.
+ - name: Install the released commit-check
run: |
python -m pip install --upgrade pip
- python -m pip install pytest
- python -m pip install "commit-check @ git+https://github.com/commit-check/commit-check@main"
+ python -m pip install pytest commit-check
- name: Check the docs against the package
run: pytest tests/ -q
diff --git a/docs/changelog.md b/docs/changelog.md
index 01fe8fa..1ca8a19 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -4,7 +4,25 @@ All **notable changes** to this project will be documented in this file.
Full changelog available at [GitHub releases](https://github.com/commit-check/commit-check/releases).
-## v2.13.0 (2026-08-04)
+## Highlights
+
+The releases worth knowing about, newest first. Each links to its full entry
+below and to the page that documents the feature properly.
+
+| Version | What changed | Documented in |
+|---|---|---|
+| [2.13.0](#v2130) | Stable rule IDs in terminal output and JSON | [Rules reference](rules.md) |
+| [2.12.0](#v2120) | Author name and email patterns became configurable | [CC101](rules.md#cc101) · [CC102](rules.md#cc102) |
+| [2.11.0](#v2110) | AI attribution policy | [Policy guides](guides/policies.md#ai-attribution) |
+| [2.10.0](#v2100) | `dependabot/` and `renovate/` branches accepted by default | [CC201](rules.md#cc201) |
+| [2.9.0](#v290) | AI agent branch prefixes accepted by default | [CC201](rules.md#cc201) |
+| [2.8.0](#v280) | Custom `message_pattern`; Python 3.9 dropped | [CC001](rules.md#cc001) |
+| [2.7.0](#v270) | Force push blocking | [CC301](rules.md#cc301) |
+| [2.6.0](#v260) | `--format json`, `--compact`, `--no-banner` | [Command-line recipes](example.md#output-for-scripts-and-ci) |
+| [2.5.0](#v250) | Organization-wide config with `inherit_from` | [Integrations](guides/integrations.md#across-an-organization) |
+| [2.0.0](#v200) | Configuration moved from YAML to TOML — breaking | [Migrating from v1](migration.md) |
+
+## v2.13.0 (2026-08-04) { #v2130 }
### New Features
@@ -35,11 +53,12 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
* The documentation, the landing page and the blog moved to
[commit-check.com](https://commit-check.com) and are published from one
domain. `docs.commit-check.com` URLs redirect.
- See PRs [#515](https://github.com/commit-check/commit-check/pull/515),
+ See PRs [#514](https://github.com/commit-check/commit-check/pull/514),
+ [#515](https://github.com/commit-check/commit-check/pull/515),
[#518](https://github.com/commit-check/commit-check/pull/518) and
[#519](https://github.com/commit-check/commit-check/pull/519).
-## v2.12.2 (2026-08-01)
+## v2.12.2 (2026-08-01) { #v2122 }
### Bug Fixes
@@ -53,7 +72,7 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
rejected by `subject_imperative`.
See PR [#496](https://github.com/commit-check/commit-check/pull/496).
-## v2.12.1 (2026-07-31)
+## v2.12.1 (2026-07-31) { #v2121 }
### Bug Fixes
@@ -63,13 +82,13 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
* Hardened the `pip install` step flagged by SonarCloud code scanning.
See PR [#479](https://github.com/commit-check/commit-check/pull/479).
-## v2.12.0 (2026-07-24)
+## v2.12.0 (2026-07-24) { #v2120 }
### New Features
-* **Configurable author patterns** — `author_name` and `author_email` accept a
- custom regex, so organisations can require their own naming or email domain
- instead of the built-in patterns.
+* **Configurable author patterns** — `author_name_pattern` and
+ `author_email_pattern` accept a custom regex, so organisations can require
+ their own naming or email domain instead of the built-in patterns.
See PR [#459](https://github.com/commit-check/commit-check/pull/459).
### Bug Fixes
@@ -82,12 +101,17 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
to the last commit's author. A misconfigured identity used to pass whenever
the previous commit happened to be valid.
See PR [#461](https://github.com/commit-check/commit-check/pull/461).
+
+## v2.11.1 (2026-07-09) { #v2111 }
+
+### Bug Fixes
+
* `require_signed_off_by` accepts any name, and is skipped for authors listed
in `ignore_authors`.
See PRs [#462](https://github.com/commit-check/commit-check/pull/462) and
[#464](https://github.com/commit-check/commit-check/pull/464).
-## v2.11.0 (2026-07-06)
+## v2.11.0 (2026-07-06) { #v2110 }
### New Features
@@ -108,7 +132,7 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
* Migrated PyPI publishing to `pypa/gh-action-pypi-publish`
* Removed OpenSSF Scorecard badge after evaluation (moved to Scorecard dashboard)
-## v2.10.1 (2026-06-30)
+## v2.10.1 (2026-06-30) { #v2101 }
### Bug Fixes
@@ -126,7 +150,7 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
* Removed legacy YAML config parsing code from `util.py`.
See PR [#444](https://github.com/commit-check/commit-check/pull/444).
-## v2.10.0 (2026-06-26)
+## v2.10.0 (2026-06-26) { #v2100 }
### New Features
@@ -135,7 +159,7 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
so dependency update branches are automatically recognized.
See PR [#442](https://github.com/commit-check/commit-check/pull/442).
-## v2.9.0 (2026-06-22)
+## v2.9.0 (2026-06-22) { #v290 }
### New Features
@@ -144,7 +168,7 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
`DEFAULT_BRANCH_TYPES` so branches created by AI coding agents are
recognized as valid. See PR [#438](https://github.com/commit-check/commit-check/pull/438).
-## v2.8.1 (2026-06-22)
+## v2.8.1 (2026-06-22) { #v281 }
### Chores
@@ -154,7 +178,7 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
* Added SchemaStore IDE autocompletion support for `cchk.toml`.
See PR [#433](https://github.com/commit-check/commit-check/pull/433).
-## v2.8.0 (2026-06-13)
+## v2.8.0 (2026-06-13) { #v280 }
### New Features
@@ -170,7 +194,7 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
`py.typed` marker added for downstream type checkers.
See PR [#424](https://github.com/commit-check/commit-check/pull/424).
-## v2.7.1 (2026-06-08)
+## v2.7.1 (2026-06-08) { #v271 }
### Chores
@@ -178,7 +202,7 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
* Added commit-check vs GitHub Rulesets comparison table to the README.
See PR [#419](https://github.com/commit-check/commit-check/pull/419).
-## v2.7.0 (2026-05-16)
+## v2.7.0 (2026-05-16) { #v270 }
### New Features
@@ -203,7 +227,7 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
engineering, general), growing the total from 234 to 390.
See PR [#414](https://github.com/commit-check/commit-check/pull/414).
-## v2.6.0 (2026-04-20)
+## v2.6.0 (2026-04-20) { #v260 }
### New Features
@@ -214,7 +238,7 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
* Fixed `print_error_header` state handling so repeated validations stay consistent when `--compact` is used.
-## v2.5.0 (2026-04-03)
+## v2.5.0 (2026-04-03) { #v250 }
### New Features
@@ -226,7 +250,7 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co
* Fixed incorrect mock target in `test_main_with_message_empty_string_no_stdin_with_git`: was patching `commit_check.util.get_commit_info` (ineffective) instead of `commit_check.engine.get_commit_info`.
-## v2.0.0 (2025-10-01)
+## v2.0.0 (2025-10-01) { #v200 }
!!! warning
@@ -262,10 +286,10 @@ Architecture Redesign:
See PR [#280](https://github.com/commit-check/commit-check/pull/280)
-## v0.10.2 (2025-08-26)
+## v0.10.2 (2025-08-26) { #v0102 }
Last release before the big v2.0 changes.
-## v0.1.0 (2022-11-02)
+## v0.1.0 (2022-11-02) { #v010 }
Initial release of commit-check.
diff --git a/docs/configuration.md b/docs/configuration.md
index 736511e..04272c0 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -188,7 +188,7 @@ The primary use case for CLI arguments is configuring commit-check in `.pre-comm
```yaml
repos:
- repo: https://github.com/commit-check/commit-check
- rev: v2.5.0
+ rev: v2.13.0
hooks:
- id: check-message
args:
diff --git a/docs/example.md b/docs/example.md
index 00bcd9a..1c38170 100644
--- a/docs/example.md
+++ b/docs/example.md
@@ -1,8 +1,8 @@
# Command-line recipes
-Ways to invoke the checks directly. For wiring them into a workflow, see the
-[pre-commit](guides/pre-commit.md) and [GitHub Actions](guides/github-actions.md)
-guides instead — those cover the setup this page assumes you already have.
+Ways to invoke the checks directly. For wiring them into a workflow, see
+[Integrations](guides/integrations.md) instead — that covers the setup this page
+assumes you already have.
Every option, and the environment variable and TOML key that set it, is listed
in [Configuration](configuration.md).
@@ -104,7 +104,7 @@ pushed:
```yaml title=".pre-commit-config.yaml"
repos:
- repo: https://github.com/commit-check/commit-check
- rev: v2.11.0
+ rev: v2.13.0
hooks:
- id: check-no-force-push
stages: [pre-push]
diff --git a/docs/getting-started/quickstart.md b/docs/getting-started.md
similarity index 65%
rename from docs/getting-started/quickstart.md
rename to docs/getting-started.md
index e3deb6d..b3af7d4 100644
--- a/docs/getting-started/quickstart.md
+++ b/docs/getting-started.md
@@ -1,16 +1,47 @@
-# Quick start
+# Getting started
-By the end of this page you will have Commit Check rejecting a bad commit
-message on your machine, and you will understand what it is telling you.
+By the end of this page you will have Commit Check installed, rejecting a bad
+commit message on your machine, and wired into the two places it belongs.
It takes about five minutes and needs nothing but a Git repository.
## 1. Install
+=== "pip"
+
+ ```console
+ $ pip install commit-check
+ ```
+
+=== "uv"
+
+ ```console
+ $ uv tool install commit-check
+ ```
+
+=== "pipx"
+
+ ```console
+ $ pipx install commit-check
+ ```
+
+Verify the install:
+
```console
-$ pip install commit-check
+$ commit-check --version
```
+The CLI is also available as `cchk`, which is the same program under a shorter
+name.
+
+!!! tip "Supported Python versions"
+
+ Commit Check supports Python 3.10 through 3.14, on Linux, macOS and Windows.
+
+Running it as a [pre-commit hook](guides/integrations.md#as-a-pre-commit-hook)
+or a [GitHub Action](guides/integrations.md#in-github-actions) needs no
+installation step at all — both fetch it for you.
+
## 2. Watch it reject something
Commit Check works with no configuration at all. Make a deliberately bad commit
@@ -77,8 +108,8 @@ $ commit-check --branch
`commit-check --message` never evaluates branch rules, and vice versa. Each
check is selected by its own flag, so you can run exactly what a given hook
- or CI job needs. The
- [rules reference](../rules.md) lists which flag activates each rule.
+ or CI job needs. The [rules reference](rules.md) lists which flag activates
+ each rule.
## 5. Write down your policy
@@ -109,8 +140,8 @@ $ commit-check --message
Even with no config file, Conventional Commits, Conventional Branch, subject
length limits of 5–80 characters, and author name/email patterns are
- enforced. Check the *Default* column in the
- [rules reference](../rules.md) before assuming a rule is off.
+ enforced. Check the *Default* column in the [rules reference](rules.md)
+ before assuming a rule is off.
## 6. Make it automatic
@@ -126,7 +157,7 @@ belongs:
A pre-commit hook rejects the message as you write it, so nothing bad
reaches the branch in the first place.
- [:octicons-arrow-right-24: Pre-commit guide](../guides/pre-commit.md)
+ [:octicons-arrow-right-24: Pre-commit hook](guides/integrations.md#as-a-pre-commit-hook)
- :material-github:{ .lg .middle } __On every pull request__
@@ -135,14 +166,24 @@ belongs:
A GitHub Action checks every commit in the PR and can comment on the PR
with what needs fixing.
- [:octicons-arrow-right-24: GitHub Actions guide](../guides/github-actions.md)
+ [:octicons-arrow-right-24: GitHub Actions](guides/integrations.md#in-github-actions)
+## Verifying the download
+
+Releases are built with [SLSA Level 3](https://slsa.dev) provenance. To verify a
+release artifact came from this repository's build pipeline:
+
+```console
+$ gh attestation verify commit_check-*.whl --repo commit-check/commit-check
+```
+
## Where to go next
-- **[Rules reference](../rules.md)** — every rule, what it does, why it matters,
+- **[Rules reference](rules.md)** — every rule, what it does, why it matters,
and how to configure it.
-- **[Configuration](../configuration.md)** — every option, its type and default,
+- **[Configuration](configuration.md)** — every option, its type and default,
plus the environment variable and CLI flag that override it.
-- **[Why Commit Check](why.md)** — the reasoning behind the tool.
+- **[Command-line recipes](example.md)** — checking a range, wiring up CI,
+ reading the JSON output.
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
deleted file mode 100644
index c915fc6..0000000
--- a/docs/getting-started/installation.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# Installation
-
-Commit Check runs anywhere Python does, and ships as a GitHub Action and an MCP
-server for the places it doesn't.
-
-## Command line
-
-=== "pip"
-
- ```console
- $ pip install commit-check
- ```
-
-=== "uv"
-
- ```console
- $ uv tool install commit-check
- ```
-
-=== "pipx"
-
- ```console
- $ pipx install commit-check
- ```
-
-Verify the install:
-
-```console
-$ commit-check --version
-```
-
-The CLI is also available as `cchk`, which is the same program under a shorter
-name.
-
-!!! tip "Supported Python versions"
-
- Commit Check supports Python 3.10 through 3.14, on Linux, macOS and Windows.
-
-## As a pre-commit hook
-
-No installation step — [pre-commit](https://pre-commit.com) fetches it for you.
-See the [pre-commit guide](../guides/pre-commit.md).
-
-## As a GitHub Action
-
-No installation step. See the
-[GitHub Actions guide](../guides/github-actions.md).
-
-## Verifying the download
-
-Releases are built with [SLSA Level 3](https://slsa.dev) provenance. To verify a
-release artifact came from this repository's build pipeline:
-
-```console
-$ gh attestation verify commit_check-*.whl --repo commit-check/commit-check
-```
-
-## Next steps
-
-
-
-- :material-rocket-launch-outline:{ .lg .middle } __Quick start__
-
- ---
-
- Catch your first bad commit in five minutes.
-
- [:octicons-arrow-right-24: Quick start](quickstart.md)
-
-- :material-book-open-variant:{ .lg .middle } __Rules reference__
-
- ---
-
- Every rule, what it does, and why it matters.
-
- [:octicons-arrow-right-24: Rules](../rules.md)
-
-
diff --git a/docs/getting-started/why.md b/docs/getting-started/why.md
deleted file mode 100644
index f114312..0000000
--- a/docs/getting-started/why.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# Why Commit Check
-
-## The problem
-
-Git history is a database that every team writes to and almost nobody validates.
-
-The cost shows up later, and indirectly. Release notes get written by hand
-because commit subjects cannot be grouped. `git bisect` walks through merge
-commits that record nothing but a sync. A commit is attributed to `ec2-user`
-because a build box had no `user.name`. A contribution has to be rejected
-months after the fact because it never carried a `Signed-off-by` trailer.
-
-None of these are caught by a linter, a type checker, or a test suite. They are
-all caught by review, which means they are caught inconsistently, by whoever
-happens to be looking, and only after the work is done.
-
-## The approach
-
-Commit Check treats commit metadata the way linters treat code: a policy written
-down once, enforced identically everywhere, with a stable identifier for every
-diagnostic so that findings can be discussed, suppressed, and tracked.
-
-**One config.** A single `cchk.toml` drives the CLI, the pre-commit hook, the
-GitHub Action, and the MCP server. There is no second place where the rules can
-disagree with themselves.
-
-**Fails where it is cheap.** The same check that runs in CI runs in your
-`commit-msg` hook. Finding out that a subject is malformed takes a second
-locally and a full CI cycle plus a force-push remotely.
-
-**Stable rule IDs.** Every rule has an ID like [CC003](../rules.md#cc003) that
-never changes once released. You can cite it in a review comment, link to its
-documentation, and eventually suppress it per-rule.
-
-**Explains itself.** A failure names the rule, quotes the offending value, says
-how to fix it, and links to the reasoning.
-
-## Where it fits
-
-Commit Check is deliberately narrow: it validates *metadata*, not code. It is a
-lightweight, open alternative to
-[GitHub Enterprise metadata restrictions](https://docs.github.com/en/enterprise-server@3.11/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#metadata-restrictions)
-and Bitbucket's paid
-[Yet Another Commit Checker](https://marketplace.atlassian.com/apps/1211854/yet-another-commit-checker),
-without requiring a particular forge or an enterprise plan.
-
-If you already run `ruff`, `eslint`, or `golangci-lint` on your source, Commit
-Check is the equivalent for the commits that carry it.
-
-## What it is not
-
-- **Not a code linter.** It never reads your source files.
-- **Not a replacement for review.** It enforces the mechanical rules so review
- can spend its attention on the change itself.
-- **Not opinionated by default.** Most rules are off until you turn them on. See
- the [rules reference](../rules.md) for what applies out of the box.
diff --git a/docs/guides/ai-attribution.md b/docs/guides/ai-attribution.md
deleted file mode 100644
index 7ffdac7..0000000
--- a/docs/guides/ai-attribution.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# Set an AI attribution policy
-
-AI coding tools add trailers to commit messages identifying themselves. Whether
-that is welcome, required, or unacceptable is a decision each project makes for
-itself — and the industry has landed in different places:
-
-- The **Linux kernel** added an `Assisted-by:` trailer, treating AI assistance
- as something to disclose.
-- **Some projects disallow AI-assisted contributions outright**, usually over
- provenance and licensing.
-- **Most projects have no stated position**, which means the question resurfaces
- in every code review.
-
-Commit Check does not take a side. It gives you a way to enforce whichever
-position your project has already taken, so it stops being relitigated.
-
-## The default: no opinion
-
-```toml
-[commit]
-ai_attribution = "ignore" # the default
-```
-
-[CC013](../rules.md#cc013) is off. Commits carrying AI trailers pass, and so do
-commits without them.
-
-## Forbidding AI-attributed commits
-
-```toml title="cchk.toml"
-[commit]
-ai_attribution = "forbid"
-```
-
-Commits carrying a recognised AI signature now fail:
-
-```text
-CC013 ai-attribution check failed ==> feat: add caching layer
-AI attribution policy violation
-Suggest: This project forbids AI-assisted commits. Remove AI trailers and re-commit.
-Docs: https://commit-check.com/rules/#cc013
-```
-
-### What counts as a signature
-
-Trailers and co-author lines naming Claude Code, GitHub Copilot, Codex, Gemini,
-Cursor, Devin, Aider, Windsurf and Tabby, plus generic AI model patterns.
-
-!!! warning "This checks disclosure, not authorship"
-
- CC013 reads commit metadata. It detects a commit that *says* it was
- AI-assisted; it cannot detect one that was AI-assisted and did not say so.
-
- Set against a policy of "no AI contributions", it is an honesty check on
- contributors who are already following the rules — not an enforcement
- mechanism against those who aren't. Be clear with yourself about which of
- those you are buying.
-
-## Exempting automation
-
-Bots that legitimately carry AI trailers can be excluded:
-
-```toml title="cchk.toml"
-[commit]
-ai_attribution = "forbid"
-ignore_authors = ["dependabot[bot]", "renovate[bot]"]
-```
-
-## Documenting the decision
-
-Whichever way you go, the config file is not where contributors look. State the
-policy where they will see it — `CONTRIBUTING.md`, the pull request template —
-and let Commit Check be the mechanism rather than the announcement.
-
-Enforcing an undocumented policy produces a confusing failure for somebody
-acting in good faith.
diff --git a/docs/guides/github-actions.md b/docs/guides/github-actions.md
deleted file mode 100644
index 4dc95ae..0000000
--- a/docs/guides/github-actions.md
+++ /dev/null
@@ -1,102 +0,0 @@
-# Run in GitHub Actions
-
-Local hooks can be skipped with `--no-verify`. A CI check cannot, which makes
-GitHub Actions the place where your policy is actually a policy.
-
-## Minimal setup
-
-```yaml title=".github/workflows/commit-check.yml"
-name: Commit Check
-
-on:
- push:
- pull_request:
- branches: [main]
-
-jobs:
- commit-check:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- steps:
- - uses: actions/checkout@v5
- with:
- ref: ${{ github.event.pull_request.head.sha }}
- fetch-depth: 0 # (1)!
- - uses: commit-check/commit-check-action@v1
- with:
- message: true
- branch: true
- author-name: true
- author-email: true
-```
-
-1. Commit Check needs the full history to inspect every commit in the pull
- request. Without this it only sees the most recent one.
-
-## Commenting on the pull request
-
-Instead of making contributors open the job log, have the Action post what
-needs fixing directly on the PR:
-
-```yaml
- - uses: commit-check/commit-check-action@v1
- with:
- message: true
- branch: true
- pr-comments: ${{ github.event_name == 'pull_request' }}
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-```
-
-This needs extra permissions on the job:
-
-```yaml
- permissions:
- contents: read
- pull-requests: write
-```
-
-## Reporting without failing
-
-While a team is adopting the policy, it is often better to report problems
-without blocking merges. `dry-run` always exits `0`:
-
-```yaml
- - uses: commit-check/commit-check-action@v1
- with:
- message: true
- dry-run: true
-```
-
-Turn it off once the history is clean.
-
-## Sharing config with local hooks
-
-The Action reads the same `cchk.toml` as the CLI, so a repository that already
-has one needs no Action-specific configuration. That is the point: the rules
-cannot drift between what a developer sees locally and what CI enforces.
-
-See [Configuration](../configuration.md) for where the file may live, and
-[Organization-wide policy](organization.md) for sharing one across repositories.
-
-## Pull requests from forks
-
-A `pull_request` workflow triggered by a fork receives a **read-only**
-`GITHUB_TOKEN`, and `permissions: pull-requests: write` does not override that.
-`pr-comments` will therefore fail to post on fork pull requests unless the
-repository has *Send write tokens to workflows from pull requests* enabled under
-**Settings → Actions → General**.
-
-!!! warning "Do not reach for `pull_request_target` casually"
-
- `pull_request_target` does get a write token, but it runs in the context of
- the base repository with access to its secrets. Checking out and executing
- the fork's code under that trigger is the "pwn request" pattern and hands
- repository access to anyone who can open a pull request.
-
- If you use it, check out the base branch only and never run code from the
- pull request.
-
-The checks themselves still run on fork pull requests and still fail the build;
-only the commenting is affected.
diff --git a/docs/guides/integrations.md b/docs/guides/integrations.md
new file mode 100644
index 0000000..e108a11
--- /dev/null
+++ b/docs/guides/integrations.md
@@ -0,0 +1,294 @@
+# Integrations
+
+Commit Check runs in three places, and all three read the same `cchk.toml`.
+That is the point: the rules cannot drift between what a developer sees locally
+and what CI enforces.
+
+| Where | Catches problems | Can be bypassed |
+|---|---|---|
+| [Pre-commit hook](#as-a-pre-commit-hook) | As the message is written | Yes — `--no-verify` |
+| [GitHub Actions](#in-github-actions) | On every pull request | No |
+| [Organization config](#across-an-organization) | Everywhere at once | — |
+
+A hook gives fast feedback to people who want to follow the policy. The Action
+is what makes it a policy. Most projects want both.
+
+## As a pre-commit hook
+
+A pre-commit hook is the cheapest place to enforce commit policy: the developer
+finds out while they are still writing the message, not after a CI round trip.
+
+Add Commit Check to `.pre-commit-config.yaml`:
+
+```yaml title=".pre-commit-config.yaml"
+repos:
+ - repo: https://github.com/commit-check/commit-check
+ rev: v2.13.0
+ hooks:
+ - id: check-message
+ - id: check-branch
+ - id: check-author-name
+ - id: check-author-email
+```
+
+Then install the hooks. `check-message` runs at the `commit-msg` stage, so it
+needs its own install step:
+
+```console
+$ pre-commit install --hook-type commit-msg
+$ pre-commit install
+```
+
+That is it. The next malformed commit message is rejected before it exists.
+
+!!! warning "If `check-message` never runs"
+
+ It is almost always because `pre-commit install --hook-type commit-msg` was
+ not run — a plain `pre-commit install` only wires up the `pre-commit` stage.
+ More in [Troubleshooting](../troubleshoot.md).
+
+### Available hooks
+
+| Hook ID | Stage | Rules |
+|---|---|---|
+| `check-message` | `commit-msg` | [CC001–CC013](../rules.md#commit-message-rules) |
+| `check-branch` | `pre-commit` | [CC201–CC202](../rules.md#branch-rules) |
+| `check-author-name` | `pre-commit` | [CC101](../rules.md#cc101) |
+| `check-author-email` | `pre-commit` | [CC102](../rules.md#cc102) |
+| `check-no-force-push` | `pre-push` | [CC301](../rules.md#cc301) |
+
+`check-no-force-push` also needs its own install:
+
+```console
+$ pre-commit install --hook-type pre-push
+```
+
+### Configuring without a TOML file
+
+Options can be passed as hook arguments, which keeps everything in one file:
+
+```yaml title=".pre-commit-config.yaml"
+repos:
+ - repo: https://github.com/commit-check/commit-check
+ rev: v2.13.0
+ hooks:
+ - id: check-message
+ args:
+ - --subject-imperative=true
+ - --subject-max-length=72
+ - --allow-merge-commits=false
+```
+
+A `cchk.toml` is usually the better choice once you have more than a couple of
+options, because CI and the CLI read it too. See
+[Configuration](../configuration.md) for the precedence rules.
+
+### Skipping a hook
+
+Occasionally you need to get a commit through — a mid-rebase fixup, an
+automated migration. `pre-commit` supports this natively:
+
+```console
+$ SKIP=check-message git commit -m "wip"
+```
+
+!!! warning "Local hooks are not a policy boundary"
+
+ Anyone can pass `--no-verify`. Hooks exist to give fast feedback to people
+ who want to follow the policy, not to stop people who don't. Pair them with
+ the [GitHub Action](#in-github-actions), which runs where it cannot be
+ skipped.
+
+## In GitHub Actions
+
+Local hooks can be skipped. A CI check cannot, which makes GitHub Actions the
+place where your policy is actually a policy.
+
+```yaml title=".github/workflows/commit-check.yml"
+name: Commit Check
+
+on:
+ push:
+ pull_request:
+ branches: [main]
+
+jobs:
+ commit-check:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ fetch-depth: 0 # (1)!
+ - uses: commit-check/commit-check-action@v2
+ with:
+ message: true
+ branch: true
+ author-name: true
+ author-email: true
+```
+
+1. Commit Check needs the full history to inspect every commit in the pull
+ request. Without this it only sees the most recent one.
+
+The Action reads the same `cchk.toml` as the CLI, so a repository that already
+has one needs no Action-specific configuration.
+
+### Commenting on the pull request
+
+Instead of making contributors open the job log, have the Action post what
+needs fixing directly on the PR:
+
+```yaml
+ - uses: commit-check/commit-check-action@v2
+ with:
+ message: true
+ branch: true
+ pr-comments: ${{ github.event_name == 'pull_request' }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+```
+
+This needs extra permissions on the job:
+
+```yaml
+ permissions:
+ contents: read
+ pull-requests: write
+```
+
+### Reporting without failing
+
+While a team is adopting the policy, it is often better to report problems
+without blocking merges. `dry-run` always exits `0`:
+
+```yaml
+ - uses: commit-check/commit-check-action@v2
+ with:
+ message: true
+ dry-run: true
+```
+
+Turn it off once the history is clean.
+
+### Pull requests from forks
+
+A `pull_request` workflow triggered by a fork receives a **read-only**
+`GITHUB_TOKEN`, and `permissions: pull-requests: write` does not override that.
+`pr-comments` will therefore fail to post on fork pull requests unless the
+repository has *Send write tokens to workflows from pull requests* enabled under
+**Settings → Actions → General**.
+
+!!! warning "Do not reach for `pull_request_target` casually"
+
+ `pull_request_target` does get a write token, but it runs in the context of
+ the base repository with access to its secrets. Checking out and executing
+ the fork's code under that trigger is the "pwn request" pattern and hands
+ repository access to anyone who can open a pull request.
+
+ If you use it, check out the base branch only and never run code from the
+ pull request.
+
+The checks themselves still run on fork pull requests and still fail the build;
+only the commenting is affected.
+
+## Across an organization
+
+Copying `cchk.toml` into forty repositories works until the day you want to
+change it. `inherit_from` lets each repository pull a shared base config and
+override only what it genuinely needs.
+
+### The shared config
+
+Put the policy in a repository every project can read — GitHub's `.github`
+repository is the conventional home:
+
+```toml title="my-org/.github → cchk.toml"
+[commit]
+conventional_commits = true
+subject_imperative = true
+subject_max_length = 72
+allow_merge_commits = false
+
+[branch]
+conventional_branch = true
+allow_branch_types = ["feature", "bugfix", "hotfix", "release", "chore"]
+```
+
+### Inheriting it
+
+Each repository then needs one line:
+
+```toml title="any-repo → .github/cchk.toml"
+inherit_from = "github:my-org/.github:cchk.toml"
+```
+
+Local settings win, so a project with a different constraint overrides just that
+one option:
+
+```toml title="a-repo-with-longer-subjects → .github/cchk.toml"
+inherit_from = "github:my-org/.github:cchk.toml"
+
+[commit]
+subject_max_length = 100 # everything else comes from the org config
+```
+
+### Pinning the version
+
+By default the shorthand resolves to the parent repository's default branch,
+which means a change to the org config takes effect everywhere on the next run.
+That is usually what you want. When it isn't, pin to a ref:
+
+```toml
+inherit_from = "github:my-org/.github@v1:cchk.toml"
+```
+
+### Other sources
+
+=== "GitHub shorthand"
+
+ ```toml
+ inherit_from = "github:my-org/.github:cchk.toml"
+ ```
+
+=== "Local path"
+
+ ```toml
+ inherit_from = "../../shared/org-cchk.toml"
+ ```
+
+ Useful in a monorepo, where the shared config is already checked out.
+
+=== "HTTPS URL"
+
+ ```toml
+ inherit_from = "https://example.com/shared/cchk.toml"
+ ```
+
+ Plain HTTP is rejected.
+
+!!! warning "Inheritance fails open"
+
+ If the parent config is unreachable — a network blip, a renamed file, a
+ private repository — Commit Check silently falls back to the local config
+ rather than failing the build. This keeps CI green during an outage, but it
+ also means a typo in `inherit_from` is easy to miss. Verify the merged
+ result when you first set it up:
+
+ ```console
+ $ commit-check --message --format json
+ ```
+
+### Rolling it out
+
+Turning on a strict policy across an organization at once produces a wall of
+red. A gentler sequence:
+
+1. Ship the org config with [`dry-run`](#reporting-without-failing) enabled in
+ CI, so violations are reported but nothing blocks.
+2. Look at what actually fails. Some rules will turn out to be wrong for some
+ teams — that is information, not an obstacle.
+3. Turn off `dry-run` for repositories whose history is already clean.
+4. Tighten the shared config over time.
diff --git a/docs/guides/organization.md b/docs/guides/organization.md
deleted file mode 100644
index 1da9fd0..0000000
--- a/docs/guides/organization.md
+++ /dev/null
@@ -1,100 +0,0 @@
-# Enforce one policy across an organization
-
-Copying `cchk.toml` into forty repositories works until the day you want to
-change it. `inherit_from` lets each repository pull a shared base config and
-override only what it genuinely needs.
-
-## The shared config
-
-Put the policy in a repository every project can read — GitHub's `.github`
-repository is the conventional home:
-
-```toml title="my-org/.github → cchk.toml"
-[commit]
-conventional_commits = true
-subject_imperative = true
-subject_max_length = 72
-allow_merge_commits = false
-
-[branch]
-conventional_branch = true
-allow_branch_types = ["feature", "bugfix", "hotfix", "release", "chore"]
-```
-
-## Inheriting it
-
-Each repository then needs one line:
-
-```toml title="any-repo → .github/cchk.toml"
-inherit_from = "github:my-org/.github:cchk.toml"
-```
-
-Local settings win, so a project with a different constraint overrides just that
-one option:
-
-```toml title="a-repo-with-longer-subjects → .github/cchk.toml"
-inherit_from = "github:my-org/.github:cchk.toml"
-
-[commit]
-subject_max_length = 100 # everything else comes from the org config
-```
-
-## Pinning the version
-
-By default the shorthand resolves to the parent repository's default branch,
-which means a change to the org config takes effect everywhere on the next run.
-That is usually what you want. When it isn't, pin to a ref:
-
-```toml
-inherit_from = "github:my-org/.github@v1:cchk.toml"
-```
-
-## Other sources
-
-=== "GitHub shorthand"
-
- ```toml
- inherit_from = "github:my-org/.github:cchk.toml"
- ```
-
-=== "Local path"
-
- ```toml
- inherit_from = "../../shared/org-cchk.toml"
- ```
-
- Useful in a monorepo, where the shared config is already checked out.
-
-=== "HTTPS URL"
-
- ```toml
- inherit_from = "https://example.com/shared/cchk.toml"
- ```
-
- Plain HTTP is rejected.
-
-!!! warning "Inheritance fails open"
-
- If the parent config is unreachable — a network blip, a renamed file, a
- private repository — Commit Check silently falls back to the local config
- rather than failing the build. This keeps CI green during an outage, but it
- also means a typo in `inherit_from` is easy to miss. Verify the merged
- result when you first set it up:
-
- ```console
- $ commit-check --message --format json
- ```
-
-## Rolling it out
-
-Turning on a strict policy across an organization at once produces a wall of
-red. A gentler sequence:
-
-1. Ship the org config with `dry-run` enabled in CI, so violations are reported
- but nothing blocks.
-2. Look at what actually fails. Some rules will turn out to be wrong for some
- teams — that is information, not an obstacle.
-3. Turn off `dry-run` for repositories whose history is already clean.
-4. Tighten the shared config over time.
-
-See the [GitHub Actions guide](github-actions.md) for the `dry-run` input.
diff --git a/docs/guides/policies.md b/docs/guides/policies.md
new file mode 100644
index 0000000..2fa3976
--- /dev/null
+++ b/docs/guides/policies.md
@@ -0,0 +1,162 @@
+# Policy guides
+
+Two rules are off by default because they encode a decision only your project
+can make: whether contributors must certify the origin of their work, and
+whether AI assistance is welcome, disclosed, or refused.
+
+Commit Check does not take a side on either. It gives you a way to enforce the
+position you have already taken, so it stops being relitigated in every review.
+
+## Require signoff (DCO)
+
+Projects that use the [Developer Certificate of Origin](https://developercertificate.org/)
+require every commit to carry a `Signed-off-by` trailer. The Linux kernel and
+much of the CNCF work this way.
+
+A DCO bot rejecting a pull request after the fact is a poor experience: the
+contributor has to rewrite history for every commit in the branch. Checking
+locally fixes it before it becomes a problem.
+
+### Turn it on
+
+```toml title="cchk.toml"
+[commit]
+require_signed_off_by = true
+```
+
+This enables [CC012](../rules.md#cc012), which is off by default.
+
+### Signing off
+
+```console
+$ git commit --signoff -m "fix: handle an empty config file"
+```
+
+The trailer is appended automatically from your `user.name` and `user.email`:
+
+```text
+fix: handle an empty config file
+
+Signed-off-by: Your Name
+```
+
+Forgot it? Fix the last commit in place:
+
+```console
+$ git commit --amend --signoff --no-edit
+```
+
+Fix a whole branch:
+
+```console
+$ git rebase --signoff main
+```
+
+!!! tip "Make it automatic"
+
+ Signing off is easy to forget. Combine this rule with the
+ [pre-commit hook](integrations.md#as-a-pre-commit-hook) so a missing trailer
+ is caught at commit time, not at review time.
+
+### Identity matters
+
+The DCO is a statement about who wrote the code, so it only means something if
+the identity is real. [CC101](../rules.md#cc101) and
+[CC102](../rules.md#cc102) check the committer name and email, and are enabled
+by default when their check runs:
+
+```console
+$ commit-check --author-name --author-email
+```
+
+To require a company address:
+
+```toml title="cchk.toml"
+[commit]
+author_email_pattern = "^.+@example\\.com$"
+```
+
+### Bots
+
+Automation cannot meaningfully sign the DCO, and forcing it to produces
+meaningless trailers. Exempt bots instead:
+
+```toml title="cchk.toml"
+[commit]
+require_signed_off_by = true
+ignore_authors = ["dependabot[bot]", "renovate[bot]"]
+```
+
+`ignore_authors` matches the commit author and any `Co-authored-by:` trailers.
+
+## AI attribution
+
+AI coding tools add trailers to commit messages identifying themselves. Whether
+that is welcome, required, or unacceptable is a decision each project makes for
+itself — and the industry has landed in different places:
+
+- The **Linux kernel** added an `Assisted-by:` trailer, treating AI assistance
+ as something to disclose.
+- **Some projects disallow AI-assisted contributions outright**, usually over
+ provenance and licensing.
+- **Most projects have no stated position**, which means the question resurfaces
+ in every code review.
+
+### The default: no opinion
+
+```toml
+[commit]
+ai_attribution = "ignore" # the default
+```
+
+[CC013](../rules.md#cc013) is off. Commits carrying AI trailers pass, and so do
+commits without them.
+
+### Forbidding AI-attributed commits
+
+```toml title="cchk.toml"
+[commit]
+ai_attribution = "forbid"
+```
+
+Commits carrying a recognised AI signature now fail:
+
+```text
+CC013 ai-attribution check failed ==> feat: add caching layer
+AI attribution policy violation
+Suggest: This project forbids AI-assisted commits. Remove AI trailers and re-commit.
+Docs: https://commit-check.com/rules/#cc013
+```
+
+Recognised signatures are trailers and co-author lines naming Claude Code,
+GitHub Copilot, Codex, Gemini, Cursor, Devin, Aider, Windsurf and Tabby, plus
+generic AI model patterns.
+
+!!! warning "This checks disclosure, not authorship"
+
+ CC013 reads commit metadata. It detects a commit that *says* it was
+ AI-assisted; it cannot detect one that was AI-assisted and did not say so.
+
+ Set against a policy of "no AI contributions", it is an honesty check on
+ contributors who are already following the rules — not an enforcement
+ mechanism against those who aren't. Be clear with yourself about which of
+ those you are buying.
+
+### Exempting automation
+
+Bots that legitimately carry AI trailers can be excluded:
+
+```toml title="cchk.toml"
+[commit]
+ai_attribution = "forbid"
+ignore_authors = ["dependabot[bot]", "renovate[bot]"]
+```
+
+### Documenting the decision
+
+Whichever way you go, the config file is not where contributors look. State the
+policy where they will see it — `CONTRIBUTING.md`, the pull request template —
+and let Commit Check be the mechanism rather than the announcement.
+
+Enforcing an undocumented policy produces a confusing failure for somebody
+acting in good faith.
diff --git a/docs/guides/pre-commit.md b/docs/guides/pre-commit.md
deleted file mode 100644
index 193f0f9..0000000
--- a/docs/guides/pre-commit.md
+++ /dev/null
@@ -1,89 +0,0 @@
-# Run as a pre-commit hook
-
-A pre-commit hook is the cheapest place to enforce commit policy: the developer
-finds out while they are still writing the message, not after a CI round trip.
-
-## Setup
-
-Add Commit Check to `.pre-commit-config.yaml`:
-
-```yaml title=".pre-commit-config.yaml"
-repos:
- - repo: https://github.com/commit-check/commit-check
- rev: v2.11.0
- hooks:
- - id: check-message
- - id: check-branch
- - id: check-author-name
- - id: check-author-email
-```
-
-Then install the hooks. `check-message` runs at the `commit-msg` stage, so it
-needs its own install step:
-
-```console
-$ pre-commit install --hook-type commit-msg
-$ pre-commit install
-```
-
-That is it. The next malformed commit message is rejected before it exists.
-
-## Available hooks
-
-| Hook ID | Stage | Rules |
-|---|---|---|
-| `check-message` | `commit-msg` | [CC001–CC013](../rules.md#commit-message-rules) |
-| `check-branch` | `pre-commit` | [CC201–CC202](../rules.md#branch-rules) |
-| `check-author-name` | `pre-commit` | [CC101](../rules.md#cc101) |
-| `check-author-email` | `pre-commit` | [CC102](../rules.md#cc102) |
-| `check-no-force-push` | `pre-push` | [CC301](../rules.md#cc301) |
-
-`check-no-force-push` also needs its own install:
-
-```console
-$ pre-commit install --hook-type pre-push
-```
-
-## Configuring without a TOML file
-
-Options can be passed as hook arguments, which keeps everything in one file:
-
-```yaml title=".pre-commit-config.yaml"
-repos:
- - repo: https://github.com/commit-check/commit-check
- rev: v2.11.0
- hooks:
- - id: check-message
- args:
- - --subject-imperative=true
- - --subject-max-length=72
- - --allow-merge-commits=false
-```
-
-A `cchk.toml` is usually the better choice once you have more than a couple of
-options, because CI and the CLI read it too. See
-[Configuration](../configuration.md) for the precedence rules.
-
-## Skipping a hook
-
-Occasionally you need to get a commit through — a mid-rebase fixup, an
-automated migration. `pre-commit` supports this natively:
-
-```console
-$ SKIP=check-message git commit -m "wip"
-```
-
-!!! warning "Local hooks are not a policy boundary"
-
- Anyone can pass `--no-verify`. Hooks exist to give fast feedback to people
- who want to follow the policy, not to stop people who don't. Pair them with
- the [GitHub Action](github-actions.md), which runs where it cannot be
- skipped.
-
-## Troubleshooting
-
-If `check-message` never seems to run, it is almost always because
-`pre-commit install --hook-type commit-msg` was not run — a plain
-`pre-commit install` only wires up the `pre-commit` stage.
-
-More in [Troubleshooting](../troubleshoot.md).
diff --git a/docs/guides/signoff.md b/docs/guides/signoff.md
deleted file mode 100644
index 578c37f..0000000
--- a/docs/guides/signoff.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# Require signoff (DCO)
-
-Projects that use the [Developer Certificate of Origin](https://developercertificate.org/)
-require every commit to carry a `Signed-off-by` trailer. The Linux kernel and
-much of the CNCF work this way.
-
-A DCO bot rejecting a pull request after the fact is a poor experience: the
-contributor has to rewrite history for every commit in the branch. Checking
-locally fixes it before it becomes a problem.
-
-## Turn it on
-
-```toml title="cchk.toml"
-[commit]
-require_signed_off_by = true
-```
-
-This enables [CC012](../rules.md#cc012), which is off by default.
-
-## Signing off
-
-```console
-$ git commit --signoff -m "fix: handle an empty config file"
-```
-
-The trailer is appended automatically from your `user.name` and `user.email`:
-
-```text
-fix: handle an empty config file
-
-Signed-off-by: Your Name
-```
-
-Forgot it? Fix the last commit in place:
-
-```console
-$ git commit --amend --signoff --no-edit
-```
-
-Fix a whole branch:
-
-```console
-$ git rebase --signoff main
-```
-
-!!! tip "Make it automatic"
-
- Signing off is easy to forget. Combine this rule with the
- [pre-commit hook](pre-commit.md) so a missing trailer is caught at commit
- time, not at review time.
-
-## Identity matters
-
-The DCO is a statement about who wrote the code, so it only means something if
-the identity is real. [CC101](../rules.md#cc101) and
-[CC102](../rules.md#cc102) check the committer name and email, and are enabled
-by default when their check runs:
-
-```console
-$ commit-check --author-name --author-email
-```
-
-To require a company address:
-
-```toml title="cchk.toml"
-[commit]
-author_email_pattern = "^.+@example\\.com$"
-```
-
-## Bots
-
-Automation cannot meaningfully sign the DCO, and forcing it to produces
-meaningless trailers. Exempt bots instead:
-
-```toml title="cchk.toml"
-[commit]
-require_signed_off_by = true
-ignore_authors = ["dependabot[bot]", "renovate[bot]"]
-```
-
-`ignore_authors` matches the commit author and any `Co-authored-by:` trailers.
diff --git a/docs/index.md b/docs/index.md
index 01c36e1..adff20c 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -31,7 +31,7 @@ whatever your AI agent is committing on your behalf.
```yaml title=".pre-commit-config.yaml"
repos:
- repo: https://github.com/commit-check/commit-check
- rev: v2.11.0
+ rev: v2.13.0
hooks:
- id: check-message
- id: check-branch
@@ -63,6 +63,59 @@ whatever your AI agent is committing on your behalf.
}
```
+## Why it exists
+
+Git history is a database that every team writes to and almost nobody validates.
+
+The cost shows up later, and indirectly. Release notes get written by hand
+because commit subjects cannot be grouped. `git bisect` walks through merge
+commits that record nothing but a sync. A commit is attributed to `ec2-user`
+because a build box had no `user.name`. A contribution has to be rejected months
+after the fact because it never carried a `Signed-off-by` trailer.
+
+None of these are caught by a linter, a type checker, or a test suite. They are
+all caught by review — which means inconsistently, by whoever happens to be
+looking, and only after the work is done.
+
+Commit Check treats commit metadata the way linters treat code: a policy written
+down once, enforced identically everywhere, with a stable identifier for every
+diagnostic so findings can be discussed, cited, and tracked.
+
+
+
+- :material-file-cog-outline:{ .lg .middle } __One config__
+
+ ---
+
+ A single `cchk.toml` drives the CLI, the pre-commit hook, the GitHub Action
+ and the MCP server. There is no second place where the rules can disagree
+ with themselves.
+
+- :material-lightning-bolt-outline:{ .lg .middle } __Fails where it is cheap__
+
+ ---
+
+ The same check that runs in CI runs in your `commit-msg` hook. A malformed
+ subject costs a second locally, or a full CI cycle plus a force-push
+ remotely.
+
+- :material-tag-outline:{ .lg .middle } __Stable rule IDs__
+
+ ---
+
+ Every rule has an ID like `CC003` that never changes once released. Cite it
+ in a review comment, link to its documentation, suppress it per-rule.
+
+- :material-shield-check:{ .lg .middle } __Built to be trusted__
+
+ ---
+
+ SLSA Level 3 build provenance with artifact attestation you can verify
+ before installing. A failure names the rule, quotes the offending value,
+ and says how to fix it.
+
+
+
## What it checks
@@ -101,7 +154,7 @@ whatever your AI agent is committing on your behalf.
Require the `Signed-off-by` trailer locally, so contributors find out before
CI rejects the pull request.
- [:octicons-arrow-right-24: Signoff guide](guides/signoff.md)
+ [:octicons-arrow-right-24: Policy guides](guides/policies.md#require-signoff-dco)
- :material-robot-outline:{ .lg .middle } __AI attribution__
@@ -110,7 +163,7 @@ whatever your AI agent is committing on your behalf.
Whatever your project has decided about AI-assisted commits, enforce it
mechanically instead of relitigating it in review.
- [:octicons-arrow-right-24: AI attribution guide](guides/ai-attribution.md)
+ [:octicons-arrow-right-24: Policy guides](guides/policies.md#ai-attribution)
- :material-office-building-outline:{ .lg .middle } __Org-wide policy__
@@ -119,34 +172,88 @@ whatever your AI agent is committing on your behalf.
Inherit a base config from a shared repository, then let each project
override only what it needs.
- [:octicons-arrow-right-24: Organization guide](guides/organization.md)
+ [:octicons-arrow-right-24: Integrations](guides/integrations.md#across-an-organization)
-## Built to be trusted
+## What it is not
+
+Commit Check is deliberately narrow: it validates *metadata*, not code.
+
+- **Not a code linter.** It never reads your source files.
+- **Not a replacement for review.** It enforces the mechanical rules so review
+ can spend its attention on the change itself.
+- **Not opinionated by default.** Most rules are off until you turn them on. See
+ the [rules reference](rules.md) for what applies out of the box.
+
+It is a lightweight, open alternative to
+[GitHub Enterprise metadata restrictions](https://docs.github.com/en/enterprise-server@3.11/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#metadata-restrictions)
+and Bitbucket's paid
+[Yet Another Commit Checker](https://marketplace.atlassian.com/apps/1211854/yet-another-commit-checker),
+without requiring a particular forge or an enterprise plan. If you already run
+`ruff`, `eslint` or `golangci-lint` on your source, Commit Check is the
+equivalent for the commits that carry it.
+
+## Ecosystem
+
+One policy engine, multiple enforcement surfaces. Write your `cchk.toml` once —
+every surface reads the same file.
+
+```mermaid
+graph TB
+ subgraph Policy["📄 cchk.toml"]
+ direction LR
+ Config[One policy file]
+ end
+
+ subgraph Engine["⚙️ commit-check
(Python core)"]
+ direction LR
+ CLI[CLI & pre-commit]
+ API[Python API]
+ end
+
+ subgraph Surfaces["🚀 Enforcement surfaces"]
+ Action[commit-check-action
GitHub Action]
+ MCP[commit-check-mcp
MCP Server]
+ end
+
+ Config --> Engine
+ CLI --> Action
+ API --> MCP
+ Action --> CI[CI Pipeline]
+ MCP --> Agent[AI Coding Agent]
+```
-- :material-shield-check:{ .lg .middle } __SLSA Level 3__
+- :fontawesome-brands-python: __commit-check__
---
- Build provenance with artifact attestation you can verify before
- installing.
+ **Core engine** — Python CLI, library and pre-commit hooks. Runs every
+ validation the other surfaces expose.
-- :material-tag-outline:{ .lg .middle } __Stable rule IDs__
+ [:octicons-arrow-right-24: Getting started](getting-started.md)
+ [:octicons-arrow-right-24: Repo](https://github.com/commit-check/commit-check)
+
+- :material-github: __commit-check-action__
---
- Every diagnostic carries an ID like `CC003` that never changes, so you can
- cite it in review, suppress it, or feed it to tooling.
+ **GitHub Action** — CI integration that posts results as check runs, job
+ summaries and pull request comments.
-- :material-source-commit:{ .lg .middle } __Used in production__
+ [:octicons-arrow-right-24: Guide](guides/integrations.md#in-github-actions)
+ [:octicons-arrow-right-24: Repo](https://github.com/commit-check/commit-check-action)
+
+- :material-robot: __commit-check-mcp__
---
- Running at Apache, Texas Instruments, Mila, and
- [many more](https://github.com/commit-check/commit-check-action/network/dependents).
+ **MCP server** — exposes the validations as structured tools for AI coding
+ agents such as Claude Code, Cursor and Copilot.
+
+ [:octicons-arrow-right-24: Repo](https://github.com/commit-check/commit-check-mcp)
@@ -233,48 +340,7 @@ whatever your AI agent is committing on your behalf.
-## Ecosystem
-
-Commit Check is a family of projects — one engine, multiple surfaces.
-Write your policy **once** in a `cchk.toml`, enforce it **everywhere**.
-
-
-
-- :fontawesome-brands-python: __commit-check__ `v2.11.0`
-
- ---
-
- **Core engine** — Python CLI, library & pre-commit hooks.
-
- :material-star: AI attribution governance, message patterns, JSON output
-
- [:octicons-arrow-right-24: Docs](getting-started/installation.md)
- [:octicons-arrow-right-24: Repo](https://github.com/commit-check/commit-check)
-
-- :material-github: __commit-check-action__ `v2.10.0`
-
- ---
-
- **GitHub Action** — seamless CI integration with PR comments.
-
- :material-star: Windows runner, PR title validation
-
- [:octicons-arrow-right-24: Docs](guides/github-actions.md)
- [:octicons-arrow-right-24: Repo](https://github.com/commit-check/commit-check-action)
-
-- :material-robot: __commit-check-mcp__ `v0.1.7`
-
- ---
-
- **MCP Server** — structured tools for AI coding agents.
-
- :material-star: AI attribution governance, message patterns
-
- [:octicons-arrow-right-24: Repo](https://github.com/commit-check/commit-check-mcp)
-
-
-
-[See all projects →](projects.md){ .md-button }
+And [many more](https://github.com/commit-check/commit-check-action/network/dependents).
## Ready in two minutes
@@ -286,8 +352,8 @@ $ commit-check --message --branch
No configuration file needed to start — sensible defaults apply immediately, and
you tighten them when you are ready.
-[Install :octicons-arrow-right-24:](getting-started/installation.md){ .md-button .md-button--primary }
-[Why Commit Check?](getting-started/why.md){ .md-button }
+[Get started :octicons-arrow-right-24:](getting-started.md){ .md-button .md-button--primary }
+[Rules reference](rules.md){ .md-button }
---
diff --git a/docs/projects.md b/docs/projects.md
deleted file mode 100644
index 8226890..0000000
--- a/docs/projects.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-hide:
- - toc
----
-
-# Projects
-
-The commit-check ecosystem is built on a simple architecture: **one policy engine,
-multiple enforcement surfaces.** Write your `cchk.toml` once — every surface
-reads the same file.
-
-```mermaid
-graph TB
- subgraph Policy["📄 cchk.toml"]
- direction LR
- Config[One policy file]
- end
-
- subgraph Engine["⚙️ commit-check
(Python core)"]
- direction LR
- CLI[CLI & pre-commit]
- API[Python API]
- end
-
- subgraph Surfaces["🚀 Enforcement surfaces"]
- Action[commit-check-action
GitHub Action]
- MCP[commit-check-mcp
MCP Server]
- end
-
- Config --> Engine
- CLI --> Action
- API --> MCP
- Action --> CI[CI Pipeline]
- MCP --> Agent[AI Coding Agent]
-```
-
-| Surface | What it does | Get started |
-|---------|-------------|-------------|
-| **commit-check** | CLI tool, pre-commit hooks, and Python library. The core engine that runs all validations. | [`commit-check/commit-check`](https://github.com/commit-check/commit-check) |
-| **commit-check-action** | GitHub Action wrapping the core engine. Posts results as check runs, job summaries, and PR comments. | [`commit-check/commit-check-action`](https://github.com/commit-check/commit-check-action) |
-| **commit-check-mcp** | MCP server that exposes validations as structured tools for AI coding agents (Claude Code, Cursor, Copilot, etc.). | [`commit-check/commit-check-mcp`](https://github.com/commit-check/commit-check-mcp) |
diff --git a/docs/what-is-new.md b/docs/what-is-new.md
deleted file mode 100644
index a64fc17..0000000
--- a/docs/what-is-new.md
+++ /dev/null
@@ -1,127 +0,0 @@
-# Release highlights
-
-The changes worth knowing about, newest first, each pointing at the page that
-documents it properly. For the full record of every change, see the
-[changelog](changelog.md).
-
-## 2.13.0 — Stable rule IDs
-
-Every diagnostic now carries an ID that never changes once released, so a
-failure can be cited in review, looked up, or matched by tooling.
-
-```console
-$ commit-check --message
-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
-Docs: https://commit-check.com/rules/#cc003
-```
-
-The ID also appears in `--format json` as `rule_id`, next to a `docs_url`. In a
-terminal that renders hyperlinks the ID *is* the link, and the `Docs:` line is
-dropped — piped output and CI logs keep it.
-
-`docs/`, `ci/`, `test/`, `refactor/`, `build/`, `perf/` and `style/` also join
-the default branch types.
-
-[:octicons-arrow-right-24: Rules reference](rules.md)
-
-## 2.12.0 — Custom author patterns
-
-`author_name` and `author_email` accept a regex of your own, so a team can
-require its own naming convention or email domain rather than the built-in
-patterns.
-
-```toml title="cchk.toml"
-[commit]
-author_email = "^.+@example\\.com$"
-```
-
-[:octicons-arrow-right-24: CC101](rules.md#cc101) ·
-[CC102](rules.md#cc102)
-
-## 2.11.0 — AI attribution policy
-
-Commits carrying the trailers AI coding tools add — Claude Code, Copilot,
-Codex, Gemini, Cursor, Devin, Aider, Windsurf, Tabby — can now be rejected.
-
-```toml title="cchk.toml"
-[commit]
-ai_attribution = "forbid" # "ignore" is the default
-```
-
-Whether AI-assisted commits are acceptable is a policy question with no single
-right answer, so this stays off until you turn it on.
-
-[:octicons-arrow-right-24: AI attribution guide](guides/ai-attribution.md) ·
-[CC013](rules.md#cc013)
-
-## 2.10.0 — Bot branch prefixes accepted by default
-
-`dependabot/` and `renovate/` branches pass branch validation without
-configuration. Automation was previously failing a check it could not satisfy.
-
-[:octicons-arrow-right-24: CC201](rules.md#cc201)
-
-## 2.9.0 — AI agent branch prefixes accepted by default
-
-`ai/`, `claude/`, `codex/`, `copilot/` and `cursor/` joined the default branch
-types, following
-[Conventional Branch v1.1.0](https://conventional-branch.github.io/).
-
-[:octicons-arrow-right-24: CC201](rules.md#cc201)
-
-## 2.8.0 — Custom message patterns
-
-`message_pattern` replaces the generated Conventional Commits regex with one of
-your own, for teams that already enforce a different format.
-
-```toml title="cchk.toml"
-[commit]
-message_pattern = "^PROJ-\\d+: .+"
-```
-
-This release also dropped Python 3.9. The minimum is now 3.10.
-
-[:octicons-arrow-right-24: CC001](rules.md#cc001) ·
-[Configuration](configuration.md)
-
-## 2.7.0 — Force push blocking
-
-A `pre-push` hook that refuses a force push to a shared branch, plus a
-`--no-force-push` flag for running the same check by hand.
-
-```toml title="cchk.toml"
-[push]
-allow_force_push = false
-```
-
-[:octicons-arrow-right-24: CC301](rules.md#cc301) ·
-[Command-line recipes](example.md#blocking-force-pushes)
-
-## 2.6.0 — Output controls for scripts and CI
-
-`--format json` for machine-readable results, `--compact` for one line per
-failure, and `--no-banner` to drop the ASCII art that only adds noise to a CI
-log.
-
-[:octicons-arrow-right-24: Command-line recipes](example.md#output-for-scripts-and-ci)
-
-## 2.5.0 — Organization-wide configuration
-
-`inherit_from` lets a repository pull a shared base config and override only
-what it needs, so a policy change no longer means editing every repository.
-
-```toml title=".github/cchk.toml"
-inherit_from = "github:my-org/.github:cchk.toml"
-```
-
-[:octicons-arrow-right-24: Organization guide](guides/organization.md)
-
-## 2.0.0 — TOML configuration
-
-The configuration format moved from YAML to TOML, the CLI was simplified, and
-settings became overridable by environment variable and command-line flag.
-This is a breaking change from 1.x.
-
-[:octicons-arrow-right-24: Migrating from v1](migration.md)
diff --git a/mkdocs.yml b/mkdocs.yml
index 23ec40d..5b6975f 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -17,6 +17,14 @@ edit_uri: edit/main/docs/
docs_dir: docs
+# The deploy workflow builds with --strict, which turns these into errors. The
+# pages cross-link by section anchor rather than by page alone, and an anchor
+# that no longer exists is invisible in a rendered page — it just scrolls to the
+# top. Failing the build is the only way that gets noticed.
+validation:
+ unrecognized_links: warn
+ anchors: warn
+
theme:
name: material
language: en
@@ -133,16 +141,10 @@ hooks:
nav:
- Home: index.md
- - Getting started:
- - Installation: getting-started/installation.md
- - Quick start: getting-started/quickstart.md
- - Why Commit Check: getting-started/why.md
+ - Getting started: getting-started.md
- Guides:
- - Pre-commit hook: guides/pre-commit.md
- - GitHub Actions: guides/github-actions.md
- - Organization-wide policy: guides/organization.md
- - Signoff and DCO: guides/signoff.md
- - AI attribution policy: guides/ai-attribution.md
+ - Integrations: guides/integrations.md
+ - Policy guides: guides/policies.md
- Command-line recipes: example.md
- Reference:
- Rules: rules.md
@@ -150,8 +152,6 @@ nav:
- About:
- Migrating from v1: migration.md
- Troubleshooting: troubleshoot.md
- - Release highlights: what-is-new.md
- Changelog: changelog.md
- - Projects: projects.md
- Blog:
- blog/index.md
diff --git a/netlify.toml b/netlify.toml
index 0d67e73..8478346 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -75,7 +75,7 @@
[[redirects]]
from = "/what-is-new.html"
- to = "/what-is-new/"
+ to = "/changelog/"
status = 301
force = true
@@ -101,7 +101,7 @@
[[redirects]]
from = "/README.html"
- to = "/getting-started/installation/"
+ to = "/getting-started/"
status = 301
force = true
@@ -117,10 +117,11 @@
status = 301
force = true
-# "Getting started" is a nav section rather than a page, so the bare directory
-# has nothing to serve. The old landing site published this URL.
-[[redirects]]
- from = "/getting-started/"
- to = "/getting-started/quickstart/"
- status = 301
- force = true
+# "Getting started" used to be a nav section with nothing at its own URL, and
+# this file redirected the bare directory to the quick start page. It is a real
+# page now, so the redirect is gone — a forced redirect here would shadow it.
+#
+# The pages that were merged into it, and the other retired URLs, are covered by
+# the stubs scripts/mkdocs_hooks.py writes into the build. Those are directory
+# URLs with no page behind them, so unlike the .html rules above they cannot
+# collide with anything and need no entry here.
diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py
index c3b30da..bb0f49b 100644
--- a/scripts/mkdocs_hooks.py
+++ b/scripts/mkdocs_hooks.py
@@ -20,15 +20,39 @@
"migration": "migration/",
"troubleshoot": "troubleshoot/",
"changelog": "changelog/",
- "what-is-new": "what-is-new/",
# The CLI reference is no longer a generated page; the flags it listed are
# documented alongside the settings that control them.
"cli_args": "configuration/",
"cli": "configuration/",
- "README": "getting-started/installation/",
+ "README": "getting-started/",
+ "what-is-new": "changelog/",
"genindex": "",
}
+#: Retired page URL -> the page that absorbed it.
+#
+# Unlike LEGACY_URLS these are directory URLs, so the stub is written to
+# ``/index.html``. Nothing is served at the old path any more, so the stub
+# cannot shadow a real page the way a flat ``rules.html`` would, and it is
+# written on every host rather than skipped on Netlify.
+#
+# Two URLs are deliberately absent: ``rules/`` and ``configuration/``. The rule
+# IDs printed by every released version of the package link to
+# ``commit-check.com/rules/#ccNNN`` — the URL is hardcoded in
+# ``commit_check/rules_catalog.py`` — so those two pages do not move.
+MOVED_URLS = {
+ "getting-started/installation": "getting-started/",
+ "getting-started/quickstart": "getting-started/",
+ "getting-started/why": "",
+ "guides/pre-commit": "guides/integrations/",
+ "guides/github-actions": "guides/integrations/",
+ "guides/organization": "guides/integrations/",
+ "guides/signoff": "guides/policies/",
+ "guides/ai-attribution": "guides/policies/",
+ "what-is-new": "changelog/",
+ "projects": "",
+}
+
# Redirect stubs for the URLs the Sphinx site served.
# The script carries the fragment across, because the links most worth keeping
# alive are the per-rule ones (``rules.html#cc003``) and a plain redirect drops
@@ -58,18 +82,27 @@
def on_post_build(config, **kwargs) -> None:
- """Write a redirect stub for each URL the Sphinx site used to serve."""
+ """Write a redirect stub for each URL this site no longer serves."""
site = Path(config["site_dir"])
- # On Netlify the stubs would be served in place of the real pages: Netlify
- # resolves ``/rules/`` to the file ``rules.html``, so the stub that
+ # Deploy previews pass their own URL in, and it may arrive without the
+ # trailing slash the targets below are joined onto.
+ base = (config["site_url"] or "/").rstrip("/") + "/"
+
+ # Retired pages first: these run on every host, Netlify included. The old
+ # path has no page of its own any more, so the stub is the only thing that
+ # can answer for it.
+ for old, target in MOVED_URLS.items():
+ stub = site / old / "index.html"
+ stub.parent.mkdir(parents=True, exist_ok=True)
+ stub.write_text(REDIRECT.format(url=base + target), encoding="utf-8")
+
+ # On Netlify the .html stubs would be served in place of the real pages:
+ # Netlify resolves ``/rules/`` to the file ``rules.html``, so the stub that
# redirects to ``/rules/`` would shadow ``rules/index.html`` and loop. The
# ``[[redirects]]`` table in netlify.toml covers the same URLs with real
- # 301s, so the stubs are only written for hosts without redirects.
+ # 301s, so these are only written for hosts without redirects.
if os.environ.get("NETLIFY") == "true":
return
- # Deploy previews pass their own URL in, and it may arrive without the
- # trailing slash the targets below are joined onto.
- base = (config["site_url"] or "/").rstrip("/") + "/"
for legacy, target in LEGACY_URLS.items():
(site / f"{legacy}.html").write_text(
REDIRECT.format(url=base + target), encoding="utf-8"