Catch bad commits before they merge — on every pull request.
Docs · Rules · CLI · GitHub App · MCP server
The GitHub Action for Commit Check,
commit policy as code for teams and AI agents. It checks every commit of a pull
request — message, branch, author, and optionally the PR title and the AI
attribution in its description — against your cchk.toml, and reports the
result in the job summary, as annotations on the run and, if you want, as a PR
comment. It can also label the pull requests that AI tools wrote.
Add a workflow, e.g. .github/workflows/commit-check.yml:
name: Commit Check on: pull_request: branches: 'main' jobs: commit-check: runs-on: ubuntu-latest permissions: # use permissions because use of pr-comments contents: read pull-requests: write steps: - uses: actions/checkout@v7 with: # Recommended: the action then reads the PR's commits from the clone. # With the default fetch-depth: 1 it asks the GitHub API instead. fetch-depth: 0 - uses: commit-check/commit-check-action@v2 with: message: true branch: true pr-title: true job-summary: true pr-comments: true
Note
fetch-depth: 0 is recommended, not required. A shallow clone holds only
GitHub's merge commit, so the action lists the pull request's commits
through the API — up to 250, with pull-requests: read — and fetches the
head commit for the author checks. If the commits cannot be listed, it warns
and checks HEAD alone; if the head commit cannot be fetched, it warns and
skips the author checks.
Runs on ubuntu-latest, macos-latest and windows-latest. Self-hosted
runners need a few tools — see Good to know.
| Input | Default | What it does |
|---|---|---|
message |
true |
Check every commit message against Conventional Commits |
branch |
true |
Check the branch name against Conventional Branch |
author-name |
false |
Check each commit's author name |
author-email |
false |
Check each commit's author email |
pr-title |
false |
Check the pull request title against Conventional Commits — the one that matters for squash merges. Pull request events only |
message-ai-attribution |
false |
Check every commit against the AI rules of cchk.toml (ai_attribution) only, author included, without the format rules. Takes the place of message |
pr-body-ai-attribution |
false |
Check the pull request description against the AI rules of cchk.toml (ai_attribution) only. Pull request events only |
ai-label |
false |
Label the pull request when an AI tool wrote any of it (a commit, the description, or an agent's app that opened it): true for ai-assisted, or the label's name. Labels are only added, never removed. Needs pull-requests: write; skipped on fork pull requests under pull_request |
pr-comments |
false |
Post the report as a pull request comment, edited in place on later runs. Needs pull-requests: write; skipped on fork pull requests |
job-summary |
true |
Write the report to the job summary |
dry-run |
false |
Report failures as warnings and always exit 0 |
Tip
pull_request does not fire when a title or description is edited. With
pr-title or pr-body-ai-attribution, add types: [opened, synchronize, reopened, edited]
to re-check an edited pull request.
The action reads the repository's cchk.toml or commit-check.toml — the same
file the CLI and the pre-commit hook use. Any setting can also come from a
CCHK_* environment variable, no config file needed:
- uses: commit-check/commit-check-action@v2 env: CCHK_SUBJECT_CAPITALIZED: "true" CCHK_REQUIRE_SIGNED_OFF_BY: "true" CCHK_AI_ATTRIBUTION: "forbid" CCHK_ALLOW_COMMIT_TYPES: "feat,fix,docs,chore"
Priority: inputs > environment variables > config file > defaults. A rule
listed under the config's top-level warn is reported in full but never fails
the run. Every key: configuration reference.
A failing run says so in its heading, gives each group one line, and shows what failed as a diff: red is what was found, green is what to write instead. Each commit and rule is linked under it, and two folds hold the rest: how to fix it (a prompt for an AI coding agent, and the git commands) and the full list.
Pull request: passed
Commits: 1 of 2 failed
Branch: failed
@@ 5584f46 @@ - bad msg # Use <type>(<scope>): <description> @@ Branch @@ - Feature/Add-Login + feature/Add-LoginCommits:
5584f46| Rules: CC001 · CC201How to fix
With an AI coding agent. Paste this prompt:
Fix these Commit Check findings without changing any code: - Commit 5584f46 "bad msg": Use <type>(<scope>): <description> - Branch: change "Feature/Add-Login" to "feature/Add-Login"Commit messages. Reword it, then force-push:
git commit --amend git push --force-with-leaseBranch name. Rename the branch and push it. A pull request cannot change its branch, so open a new one from the renamed branch:
git branch -m feature/Add-Login git push -u origin HEADNext time. The pre-commit hook runs the same rules before you push.
All 4 checks
Pull request ✔ title feat: add login page Commits ✔ d87faca feat: add login page ✖ 5584f46 bad msg CC001 message value: bad msg The commit message should follow Conventional Commits. Suggest: Use <type>(<scope>): <description> Branch ✖ branch Feature/Add-Login CC201 branch value: Feature/Add-Login The branch should follow Conventional Branch. Suggest: Rename the branch to "feature/Add-Login" (git branch -m feature/Add-Login) Fix: feature/Add-LoginChecked by Commit Check <version> · Rules · Check before you push
The rest follow the same layout:
- Passed: the heading reads "Commit Check passed", with the list folded away.
- Warned: a rule under
warngets its own diff under "Warnings", a ⚠ in the list, and counts as passed. - Skipped: a run where nothing was validated — typically a bot listed in
ignore_authors— reads "Commit Check skipped", never passed.
The step log prints the same list, each group folded under its own result, plus
one annotation per finding on the run page. With pr-comments: true the same
report is posted to the pull request, and later runs edit that one comment
instead of adding more.
With pr-body-ai-attribution: true and ai_attribution = "forbid", a
description that credits an AI co-author fails the same way, and the diff is
the line to remove:
@@ PR description @@ - Co-Authored-By: Claude <[email protected]>
With ai-label: true, a pull request an AI tool wrote also gets the
ai-assisted label.
Structured check results as JSON, available to downstream steps via
fromJSON:
- uses: commit-check/commit-check-action@v2 id: commit-check with: dry-run: true # (1) - name: Inspect results run: | echo "Status: ${{ fromJSON(steps.commit-check.outputs.result).status }}" echo "Scopes: ${{ toJSON(fromJSON(steps.commit-check.outputs.result).scopes) }}"
- Without
dry-run, a failing check ends the job before any later step runs. Usedry-run(orcontinue-on-error) when a downstream step is meant to read the result and decide for itself.
The top-level status is one of:
status |
Meaning | Exit code |
|---|---|---|
pass |
every check passed | 0 |
warn |
nothing failed, but a rule listed under the config's warn found something |
0 |
skip |
every check declined to run (for example the author is in ignore_authors) |
0 |
fail |
at least one check failed | 1 (0 with dry-run) |
Only fail is ever non-zero; warn exists so a downstream step can react to a
bent-but-not-broken policy without the run turning red:
- if: fromJSON(steps.commit-check.outputs.result).status == 'warn' run: echo "passed with warnings"
Each entry in scopes has a label (PR title, Commit 2/3, Branch, ...),
a status like the ones above, a sha (the full hash of the commit a
Commit N/M or Commit message scope checked; empty for the others) and the
check outcomes (rule_id, check, status, value, error, suggest,
fix, docs_url, reason) exactly as produced by commit-check --format json, so
downstream jobs can build their own reports or gate on individual rules.
All three run the same commit-check engine against the same
commit-check.toml / cchk.toml; they differ in where they run and what they
can see.
| GitHub Action (this repo) | pre-commit hook | Commit Check GitHub App | |
|---|---|---|---|
| Where it runs | In your workflow, on the runner, after the push | On the contributor's machine, at git commit / git push |
Hosted by commit-check; installed on the repository, no workflow file |
| What it checks | Every PR commit's message, plus the PR title, branch and author checks you enable; renders a job summary, annotations, a PR comment and the result output |
Message (commit-msg stage), branch, author; tag, force-push and files (pre-push) — one commit at a time, before it exists |
Every commit of a push or pull request: message, branch, author (the PR title only in squash mode); reported as one Commit Check check run per commit |
| When to pick it | You want enforcement in CI that a contributor cannot skip, per-rule outputs for later steps, or you run on GitHub Enterprise Server / need CCHK_* overrides |
You want the fastest feedback and to stop bad commits before they are pushed; pair it with the Action, since hooks are opt-in | You want zero YAML and no Actions minutes, or feedback on fork pull requests without the Action's read-only-token limits |
Most teams pair the pre-commit hook (fast, local) with the Action (enforced): the hook catches a bad message before it is pushed, and the Action is why CI fails when a contributor did not install the hook.
Apache
discovery-unicamp
Texas Instruments
OpenCADC
Extrawest
Chainlift
Mila
RLinf
Collective
cpp-linter
FreeBSD
JUnit
and many more.
Runner requirements
The action is a composite step and uses what the runner already has:
- Python 3.11 or newer on
PATH(python3, orpythonon Windows). The*-latestGitHub-hosted runners already have it.ubuntu-22.04ships 3.10, so addactions/setup-pythonwithpython-version: "3.11"(or newer) before this step there. Everything the action installs goes under$RUNNER_TEMP, never into your checkout. ghCLI — used to verify the build-provenance attestation of thecommit-checkwheel before installing it. Present on GitHub-hosted images; install it on self-hosted runners or the attestation step fails. Only thecommit-checkwheel is attested; PyGithub and the transitive dependencies are pinned byrequirements.txtbut not verified.- Network access to PyPI and
api.github.com— the pinned wheels are downloaded once per run and the attestation is fetched from GitHub. gitonPATH.
There is currently no input to skip attestation verification.
Fork and Dependabot pull requests
A pull request from a fork gets a read-only GITHUB_TOKEN, so pr-comments
cannot post there. The action skips the comment with a ::warning:: and
notes it in the job summary; the check, the annotations and the summary still
work. For feedback on the pull request itself, use the
Commit Check GitHub App (free on public
repositories) or run the action on pull_request_target — both are covered in
Fork pull requests.
Dependabot pull requests are not forks, but their pull_request runs also get
a read-only token by default. The pull-requests: write grant in the
usage example is honoured for them; without it the action logs a
::warning:: on the 403 and leaves the report in the job summary. Adding
dependabot[bot] to ignore_authors skips those pull requests altogether.
[](https://commit-check.com)
@v2 follows the latest v2 release; pin a full version or a commit SHA if you
prefer. Releases follow Semantic Versioning. Upgrading
from v1? See the v2.0.0 release notes.
Questions and ideas go to Discussions, bugs and feature requests to Issues.