commit-guard

commit-guard

Conventional commit linting with imperative mood detection.

$ commit-guard --range origin/main..HEAD
abc1234 feat: add user authentication
   all checks passed
def5678 wip: still working
   [subject] unknown type: wip
   [body] missing body

Install #

uv tool install git-commit-guard

Or via pre-commit.

Quick start #

# check HEAD
$ commit-guard

# check a specific commit
$ commit-guard abc1234

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

# read from a file (for git hooks)
$ commit-guard --message-file .git/COMMIT_EDITMSG

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

Checks

All checks run by default. Enable or disable individually with --enable / --disable:

Check What it verifies
subject Conventional Commits format: valid type, lowercase start, no trailing period, max length (default 72). All limits are configurable. Use ! before the colon for breaking changes: feat!: remove endpoint
imperative First word is an imperative verb — uses NLP, not just a regex
body Blank line separates subject from body, and body is non-empty
signed-off Signed-off-by: trailer is present
signature GPG or SSH signature is valid

Configuration #

Place .commit-guard.toml in your project root or any parent directory — commit-guard searches upward and uses the first file found. CLI flags always take precedence.

# .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-trailers = ["Closes", "Reviewed-by"]

Required trailers

Require arbitrary trailers in the commit message. Accepts a comma-separated list; matching is case-sensitive and requires a non-empty value after the colon (e.g. Closes: #42):

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

Range options

When using --range, merge commits are excluded by default. Use --include-merges to check them. An empty range exits non-zero by default — use --allow-empty to exit 0 instead:

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

Environment variables

Variable Default Description
COMMIT_GUARD_GIT_TIMEOUT 10 Timeout in seconds for git subprocess calls

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 | jq 'select(.ok == false)'

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.

Use --output-file FILE to write JSONL to a file while keeping human-readable text on stdout — useful in CI where you want readable logs and structured results for downstream steps:

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

GitHub Actions #

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.16.0
        with:
          range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
          disable: signed-off,signature

Check a specific commit SHA:

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

All inputs mirror the CLI flags: rev, range, enable, disable, scopes, require-scope, types, max-subject-length, min-description-length, require-trailer, allow-empty, include-merges, output-file.

When output-file is set the action exposes the path as a step output, making JSONL results available to subsequent steps:

      - uses: benner/commit-guard@v0.16.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 .pre-commit-config.yaml:

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