Skip to main content

commit-guard

PyPI version PyPI downloads CI pre-commit

Opinionated conventional commit message linter with imperative mood detection.

Why commit-guard?

  • NLP imperative detection. Descriptions must start with an imperative verb, verified via nltk POS tagging — not a hand-coded regex of "bad" words.
  • Signature verification without a local keyring. Resolves the commit committer via the GitHub API and verifies GPG/SSH against their published .gpg/.keys — no per-runner key management.
  • Strict by default. Subject format, body, trailers, Signed-off-by, and signature all enforced out of the box; opt out with --disable.

Example

$ commit-guard
   [subject] subject does not match 'type(scope): description': WIP
   [signed-off] missing 'Signed-off-by' trailer  use 'git commit -s'
   [signature] signature could not be verified  commit may be
                unsigned, or signed with a key not uploaded as a
                Signing key on https://github.com/settings/keys

Installation

From PyPI:

uv tool install git-commit-guard

or:

pipx install git-commit-guard

From a local clone:

uv tool install -e .

During development:

uv run commit-guard

Usage

# check HEAD
commit-guard

# check specific commit
commit-guard abc1234

# check commit message file (for git hooks)
commit-guard --message-file .git/COMMIT_EDITMSG

# pipe message via stdin
echo "fix(auth): add token refresh" | commit-guard

Selecting checks

All checks run by default. Use --enable or --disable with comma-separated values:

# only check subject format and imperative mood
commit-guard --enable subject,imperative

# skip body and signature checks
commit-guard --disable body,signed-off,signature

Available checks:

  • subject - Format matches type(scope): description, valid type, lowercase start, no trailing . ! ? or space, max 72 chars
  • imperative - First word is an imperative verb (for example add not added)
  • body - Blank line separates subject from body, and body is non-empty
  • signed-off - Signed-off-by: trailer exists
  • signature - Verify GPG or SSH signature via the GitHub Commits API or public key lookup

Subject length

The default maximum subject line length is 72 characters. Override with --max-subject-length:

commit-guard --max-subject-length 100

By default there is no minimum description length. Enforce one with --min-description-length:

commit-guard --min-description-length 10

Subject format

By default the description must start with a lowercase letter. To allow uppercase descriptions:

commit-guard --no-require-lowercase

In .commit-guard.toml:

require-lowercase = false

By default ., !, ?, and space are forbidden as trailing characters. To change the set (any character is valid):

commit-guard --no-trailing-chars ".,"
commit-guard --no-trailing-chars ".,!"

In .commit-guard.toml:

no-trailing-chars = [".", "!"]

Pass an empty list to disable the check entirely:

no-trailing-chars = []

Type validation

By default the standard conventional commit types are accepted. Use --types to replace the allowed set entirely:

# restrict to a subset
commit-guard --types feat,fix,chore

# add a project-specific type
commit-guard --types feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert,wip

Scope validation

By default any scope is accepted and scope is optional. Use --scopes to restrict allowed values and --require-scope to enforce that a scope is always present:

# only allow known scopes
commit-guard --scopes auth,api,db

# require a scope
commit-guard --require-scope

# combine both
commit-guard --scopes auth,api --require-scope

Required subject pattern

Require the commit subject to match a regular expression. Useful for enforcing ticket references or any custom naming convention:

commit-guard --require-subject-pattern "[A-Z]+-[0-9]+"
commit-guard --require-subject-pattern "#[0-9]+"

In .commit-guard.toml:

require-subject-pattern = "[A-Z]+-[0-9]+"

An invalid regex causes an immediate error at startup (exit 2). This check runs independently of --enable/--disable.

Required custom trailers

Require arbitrary trailers to be present in the commit message. Multiple trailers can be specified as a comma-separated list:

commit-guard --require-trailer Closes
commit-guard --require-trailer "Closes,Reviewed-by"

In .commit-guard.toml:

require-trailers = ["Closes", "Reviewed-by"]

Trailer matching is case-sensitive and requires at least one non-space character after the colon (e.g. Closes: #42). This check runs independently of --enable/--disable.

Signature verification

The signature check verifies the commit without any local keyring setup:

  1. If the repo has a GitHub remote, call the Commits API (GET /repos/{owner}/{repo}/commits/{sha}) to resolve the committer's GitHub username — this works for corporate emails, noreply addresses, or any email not listed publicly on a GitHub profile.
  2. If the Commits API is unavailable (no GitHub remote, commit not yet pushed, or API error), parse the username directly from a GitHub noreply address ({id}+{username}@users.noreply.github.com or {username}@users.noreply.github.com) — no API call needed.
  3. If neither of the above resolves a username, fall back to searching GitHub by the commit committer's email.
  4. Fetch the resolved user's public keys from github.com/{username}.gpg (GPG) and the /users/{username}/ssh_signing_keys API (SSH keys tagged with the Signing key role). Auth-only SSH keys are deliberately not accepted — this mirrors GitHub's "Verified" badge semantics.
  5. Try GPG verification: import the fetched key into a temporary keyring and run git verify-commit.
  6. Try SSH verification: write a temporary allowed_signers file and run git verify-commit with the SSH allowed-signers config.
  7. If any key verifies, the check passes. If none do, it fails.

If the committer cannot be resolved via either method, or the GitHub API is unreachable, the check fails with a clear error.

For private repositories, set GITHUB_TOKEN or GH_TOKEN so the Commits API can authenticate. The official GitHub Action wires the workflow's automatic token via the github-token input, so no manual env: is required; override with a PAT only for cross-repo lookups.

Configuration file

Place .commit-guard.toml in your project root (or any parent directory) to set defaults for enable, disable, scopes, require-scope, types, max-subject-length, min-description-length, require-lowercase, no-trailing-chars, require-subject-pattern, and require-trailers. commit-guard searches upward from the working directory and uses the first file found.

# .commit-guard.toml
disable = ["signature", "body"]
scopes = ["auth", "api", "db"]
require-scope = true
types = ["feat", "fix", "chore", "wip"]
max-subject-length = 100
min-description-length = 10
require-lowercase = false
no-trailing-chars = [".", "!"]
require-trailers = ["Closes", "Reviewed-by"]
# .commit-guard.toml
enable = ["subject", "imperative"]

CLI flags (--enable, --disable, --scopes, --require-scope, --types, --max-subject-length, --min-description-length, --no-require-lowercase, --no-trailing-chars, --require-trailer) take full precedence and ignore config file values when provided.

Environment variables

Variable Default Description
COMMIT_GUARD_GIT_TIMEOUT 10 Timeout in seconds for git subprocess calls.
GITHUB_TOKEN GitHub token for Commits API access on private repos (signature check).
GH_TOKEN Alias for GITHUB_TOKEN; used when GITHUB_TOKEN is not set.
COMMIT_GUARD_GIT_TIMEOUT=30 commit-guard --range origin/main..HEAD

In GitHub Actions, set it at the step or job level:

- uses: benner/commit-guard@v0.24.1
  env:
    COMMIT_GUARD_GIT_TIMEOUT: 30
  with:
    range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}

Checking a range of commits

Use --range to check all commits in a revision range. All commits are checked and a single non-zero exit code is returned if any fail:

# check all commits in a PR
commit-guard --range origin/main..HEAD

# check between two tags
commit-guard --range v1.0..v2.0

# only subject checks on a range
commit-guard --range origin/main..HEAD --enable subject,imperative

Merge commits are excluded by default. Use --include-merges to check them:

commit-guard --range origin/main..HEAD --include-merges

An empty range (no commits) exits non-zero by default — this catches misconfigured range specs in CI. Use --allow-empty to exit 0 instead:

commit-guard --range origin/main..HEAD --allow-empty

Quiet mode

Use --quiet (or -q) to suppress output for commits that have nothing to report — only commits with errors or warnings are printed. On a long range this leaves exactly the offending commits in the output; a fully compliant range prints nothing and exits 0:

commit-guard --range origin/main..HEAD --quiet

Quiet mode applies to single-commit and range mode, in both text and --output jsonl formats. Exit codes are unchanged, so it composes with CI gating. --output-file is not affected — the file always receives the complete record stream.

Machine-readable output

Use --output jsonl to emit one JSON line per commit to stdout instead of the default human-readable text:

commit-guard --range origin/main..HEAD --output jsonl

Each line is a JSON object:

{
  "sha": "abc1234...",
  "subject": "feat: add thing",
  "ok": false,
  "results": [{"check": "body", "level": "error", "message": "missing body"}]
}

sha is null when reading from a file or stdin. results is empty when all checks pass. Pipe to jq for filtering:

commit-guard --range origin/main..HEAD --output jsonl | jq 'select(.ok == false)'

Use --output-file FILE to write JSONL to a file while keeping human-readable text on stdout:

commit-guard --range origin/main..HEAD --output-file results.jsonl

--output-file is independent of --output: combining both writes JSONL to both stdout and the file.

In GitHub Actions, output-file is the recommended way to get machine-readable results — text stays in the CI log and the file is accessible to subsequent steps via steps.<id>.outputs.output-file.

GitHub Actions

steps:
  - uses: actions/checkout@v4
    with:
      fetch-depth: 0
  - uses: benner/commit-guard@v0.24.1

Check all commits in a pull request:

jobs:
  lint-commits:
    runs-on: ubuntu-latest
    env:
      PR_BASE: ${{ github.event.pull_request.base.sha }}
      PR_HEAD: ${{ github.event.pull_request.head.sha }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: benner/commit-guard@v0.24.1
        with:
          range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}

Check a specific commit SHA (mirrors the positional CLI argument):

      - uses: benner/commit-guard@v0.24.1
        with:
          rev: ${{ github.sha }}

All inputs are optional and mirror the CLI flags:

jobs:
  lint-commits:
    runs-on: ubuntu-latest
    env:
      PR_BASE: ${{ github.event.pull_request.base.sha }}
      PR_HEAD: ${{ github.event.pull_request.head.sha }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: benner/commit-guard@v0.24.1
        with:
          range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
          disable: signed-off,signature
          scopes: auth,api,db
          require-scope: 'true'
          require-subject-pattern: '[A-Z]+-[0-9]+'
          require-trailer: 'Closes,Reviewed-by'
          max-subject-length: '100'
          min-description-length: '10'
          no-require-lowercase: 'true'
          no-trailing-chars: '.,!'
          allow-empty: 'true'
          include-merges: 'true'
          quiet: 'true'
          output-file: results.jsonl

When output-file is set the action exposes the path as an output:

      - uses: benner/commit-guard@v0.24.1
        id: cg
        with:
          range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
          output-file: results.jsonl
      - run: jq 'select(.ok == false)' "${{ steps.cg.outputs.output-file }}"

pre-commit

Add to your .pre-commit-config.yaml:

---
repos:
  - repo: https://github.com/benner/commit-guard
    rev: v0.24.1
    hooks:
      - id: commit-guard
      - id: commit-guard-signature

Install the hooks:

pre-commit install --hook-type commit-msg --hook-type post-commit

commit-guard runs at the commit-msg stage and checks message format. commit-guard-signature runs at the post-commit stage and verifies the GPG/SSH signature after the commit object is created.

To selectively enable or disable checks, pass args:

      - id: commit-guard
        args: ["--enable", "subject,imperative"]

Imperative mood detection

commit-guard combines three strategies to detect non-imperative descriptions:

  1. nltk POS tagging — flags words tagged as past tense (VBD), gerund (VBG), third person (VBZ), etc.
  2. WordNet morphology as a fallback for words the tagger misclassifies.
  3. Hyphenated verb prefixes — accepts re-enable, auto-detect, pre-process, co-locate, under-mine and similar <prefix>-<verb> compounds the POS tagger misclassifies.

This catches common mistakes like added logging or fixes bug while keeping false positives low.

Conventional commit format

type(scope): description

body

trailers

Default types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert. Override with --types or the types config key.

Scope is optional. Mark breaking changes with ! before the colon.

License

GPLv2

Release files for git-commit-guard 0.24.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for git-commit-guard 0.24.1
File Size Uploaded
git_commit_guard-0.24.1.tar.gz 65.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for git-commit-guard 0.24.1
File Interpreter ABI Platform
git_commit_guard-0.24.1-py3-none-any.whl Python 3 none any Details

Total release size: 87.5 kB

Release files / git_commit_guard-0.24.1.tar.gz

Download URL git_commit_guard-0.24.1.tar.gz
Size 65.9 kB
Tags Source
SHA-256 checksum
How to use checksums
c73f50bdaa4086ed2ce35faac017de77286c18396b3df2677f843869fdbb213e
BLAKE2b-256 checksum
How to use checksums
096b2f0364d0e57e8e6c7e1ea8d4d979a5b374b95c801123afda162039e7df3f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.

Transparency log

Release files / git_commit_guard-0.24.1-py3-none-any.whl

Download URL git_commit_guard-0.24.1-py3-none-any.whl
Size 21.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0f3db151abd9c3d8d32096dad417f9a8c2b450143ec311ba205a85f3ae653104
BLAKE2b-256 checksum
How to use checksums
03a41718349e59ebac56598cefc643baf37d00c086802bcaa20a1a990864f69c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.24.1 This release

2 release files

0.24.0

2 release files

0.23.1

2 release files

0.23.0

2 release files

0.22.3

2 release files

0.22.2

2 release files

0.22.1

2 release files

0.16.0

2 release files

0.15.1

2 release files

0.15.0

2 release files

0.14.1

2 release files

0.14.0

2 release files

0.13.0

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page