Opinionated conventional commit message linter with imperative mood detection
Project description
commit-guard
Opinionated conventional commit message linter with imperative mood detection.
Unlike regular expression only tools, commit-guard uses NLP (nltk POS tagging) to verify that commit descriptions start with an imperative verb.
Example
$ commit-guard
✗ [subject] subject does not match 'type(scope): description': WIP
✗ [signed-off] missing 'Signed-off-by' trailer
✗ [signature] commit is not signed (GPG/SSH)
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 matchestype(scope): description, valid type, lowercase start, no trailing period, max 72 charsimperative- First word is an imperative verb (for exampleaddnotadded)body- Blank line separates subject from body, and body is non-emptysigned-off-Signed-off-by:trailer existssignature- Verify GPG or SSH signature
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 trailing . is forbidden. To change the set of forbidden trailing
characters (any character is valid, including space):
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 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.
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, 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. |
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.17.0
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
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.17.0
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.17.0
with:
range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
Check a specific commit SHA (mirrors the positional CLI argument):
- uses: benner/commit-guard@v0.17.0
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.17.0
with:
range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
disable: signed-off,signature
scopes: auth,api,db
require-scope: 'true'
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'
output-file: results.jsonl
When output-file is set the action exposes the path as an output:
- uses: benner/commit-guard@v0.17.0
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.17.0
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 two strategies to detect non-imperative descriptions:
- nltk POS tagging — flags words tagged as past tense (
VBD), gerund (VBG), third person (VBZ), etc. - WordNet morphology as a fallback for words the 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file git_commit_guard-0.17.0.tar.gz.
File metadata
- Download URL: git_commit_guard-0.17.0.tar.gz
- Upload date:
- Size: 54.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
798765feed9eab7129add460c492d7724649e4080c27fb03f208a6c1bb6f94e0
|
|
| MD5 |
b44c3942a77a47dfd2ca7190a15f859b
|
|
| BLAKE2b-256 |
d229615a69a47578f56b69c74c176447217e203ec2574724db840938c7bd4b33
|
Provenance
The following attestation bundles were made for git_commit_guard-0.17.0.tar.gz:
Publisher:
release.yml on benner/commit-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
git_commit_guard-0.17.0.tar.gz -
Subject digest:
798765feed9eab7129add460c492d7724649e4080c27fb03f208a6c1bb6f94e0 - Sigstore transparency entry: 1417176714
- Sigstore integration time:
-
Permalink:
benner/commit-guard@d4c9fbe4a203ab32f63f66e1902d780528f781f2 -
Branch / Tag:
refs/tags/v0.17.0 - Owner: https://github.com/benner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d4c9fbe4a203ab32f63f66e1902d780528f781f2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file git_commit_guard-0.17.0-py3-none-any.whl.
File metadata
- Download URL: git_commit_guard-0.17.0-py3-none-any.whl
- Upload date:
- Size: 17.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bdc68c097cdf9b9e0eb9e3053e9250fde80028f7dcdf8cecbbc3902bf9c65bbb
|
|
| MD5 |
3da9090b7fc407817aa9c0c33cfde110
|
|
| BLAKE2b-256 |
b54868af98517802f00158a3578490bec6f9d26c9151141aacef9fee312997f9
|
Provenance
The following attestation bundles were made for git_commit_guard-0.17.0-py3-none-any.whl:
Publisher:
release.yml on benner/commit-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
git_commit_guard-0.17.0-py3-none-any.whl -
Subject digest:
bdc68c097cdf9b9e0eb9e3053e9250fde80028f7dcdf8cecbbc3902bf9c65bbb - Sigstore transparency entry: 1417176729
- Sigstore integration time:
-
Permalink:
benner/commit-guard@d4c9fbe4a203ab32f63f66e1902d780528f781f2 -
Branch / Tag:
refs/tags/v0.17.0 - Owner: https://github.com/benner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d4c9fbe4a203ab32f63f66e1902d780528f781f2 -
Trigger Event:
push
-
Statement type: