Commit Lint — Conventional Commits validator
Load this skill when you need to validate git commit messages against
Conventional Commits and
produce a report of what passes and what violates the convention. It is the
quality gate of the commit pipeline: commit-message-writer writes the
commits, commit-lint checks them, version-bumper derives the next version
from the clean history.
The tool is read-only and offline: it never modifies commits, never writes
files, and makes no network calls. It reads git log of a real repository
(via subprocess), parses each commit header as `()?(!)?:
`, and reports per-commit type/scope/subject plus every violated rule.
For testability the commit list can be fed from stdin (`--stdin`, one commit
per line) — no git repository required.
---
## The linter script
`scripts/commit_lint.py` — pure Python 3 stdlib (no dependencies).
| Mode | Command |
|---|---|
| Auto-detect repo from cwd | `python3 commit_lint.py` |
| Explicit repo path | `python3 commit_lint.py /path/to/repo` |
| Multiple repos | `python3 commit_lint.py repo1 repo2 repo3` |
| Override auto-detection | `python3 commit_lint.py --repo /path/to/repo` |
| Limit to N latest commits | `python3 commit_lint.py --repo . --count 50` |
| No git needed (stdin) | `printf 'feat: ok\n' \| python3 commit_lint.py --stdin` |
| Machine-readable output | `python3 commit_lint.py --repo . --json` |
### Rules enforced (Conventional Commits v1.0.0)
| Violation | Rule | Default |
|---|---|---|
| `missing-type` | no `()?(!)?: ` prefix found | — |
| `invalid-type` | type not in the allowed set | `feat fix docs style refactor test perf ci chore build revert` |
| `type-case` | type must be lowercase (`Feat:` is a violation) | — |
| `scope-invalid` | scope must be non-empty and contain no spaces | — |
| `empty-subject` | no description after `: ` | — |
| `space-after-colon` | exactly one space required after `:` | — |
| `subject-too-long` | subject longer than max | 50 chars (`--max-subject`) |
| `header-too-long` | full header longer than max | 100 chars (`--max-header`) |
| `subject-case` | subject must start with a lowercase letter or digit | on (`--no-subject-case` disables) |
| `trailing-dot` | subject must not end with `.` | on (`--no-trailing-dot` disables) |
| `trailing-space` | subject must not end with whitespace | — |
| `body-line-too-long` | body line longer than max | 72 chars (`--max-body-line`) |
### Output
```
commit-lint report
==================
repo: /path/to/repo
generated: 2026-08-09T16:54:04
commits_analyzed: 10
clean: 5
with_violations: 5
[OK] 4a8b27c test: verify lint
[FAIL] 64cf5ff foo: unknown type
type: foo | scope: (none) | breaking: no
subject: foo: unknown type
violations:
- invalid-type: type not in allowed set: feat, fix, docs, style, refactor, test, perf, ci, chore, build, revert
...
=== Violations by type ===
invalid-type: 1
missing-type: 1
exit: 1
```
### Exit codes
| Code | Meaning |
| --- | --- |
| `0` | all analyzed commits are clean |
| `1` | at least one commit has violations |
| `2` | error (not a git repo, git not installed, bad arguments, unreadable input) |
## Usage example (typical)
```bash
# Lint the current repository (auto-detected from cwd)
python3 skills/commit-lint/scripts/commit_lint.py
# Lint a specific repository, last 50 commits
python3 skills/commit-lint/scripts/commit_lint.py --repo /path/to/repo --count 50
# CI gate: fail the pipeline when any commit violates the convention
python3 skills/commit-lint/scripts/commit_lint.py --repo . && echo "all clean" || echo "violations found"
# Machine-readable report for a bot / dashboard
python3 skills/commit-lint/scripts/commit_lint.py --repo . --json
# Test the rules without a git repo (one commit per line)
printf 'feat: add widget\nFeat: bad case\n' | python3 skills/commit-lint/scripts/commit_lint.py --stdin
```
## Interpretation guidance
- **`exit: 0`** — every analyzed commit follows the convention. The history is
ready for `version-bumper` and `changelog-generator`.
- **`exit: 1`** — violations found. Read the `[FAIL]` blocks: each lists the
exact rule that fired. Fix the commit messages (rewrite history only if the
commits are not yet shared) or adjust the limits (`--max-subject`,
`--max-header`, `--max-body-line`) to match the project's documented style.
- **`missing-type`** — the commit has no conventional prefix at all. This is
the most common finding on legacy repos; decide whether to migrate the
history or accept a baseline of non-conventional commits.
- **`invalid-type`** — the prefix exists but the type is not in the preset
table. The preset follows the friendly set from the task contract
(`feat, fix, docs, style, refactor, test, perf, ci, chore, build, revert`);
the Conventional Commits spec itself only mandates `feat` and `fix` and
leaves the rest to the project.
- **`subject-too-long` / `header-too-long`** — the 50/100 limits follow the
git commit man page and the Conventional Commits spec. Long subjects are a
readability problem: they get truncated in `git log --oneline`.
- **`subject-case`** — the subject must start with a lowercase letter or
digit. `feat: Add widget` is a violation; `feat: add widget` is not.
- **`trailing-dot`** — subjects must not end with `.`. This is a style rule
(the spec examples never use a trailing period); disable with
`--no-trailing-dot` if the project disagrees.
## Do NOT use
- Do NOT use to *rewrite* commit messages or amend history — the tool is
read-only by design. It reports; you decide what to fix.
- Do NOT use when the repository does not follow Conventional Commits at all —
the report will be mostly `missing-type` and the signal is noise. Consider
`commit-message-writer` to establish the convention first.
- Do NOT use for merge-commit validation or signed-commit verification — the
tool only reads the message text.
- Do NOT use `--stdin` when you need body checks — stdin mode carries only the
subject line; body rules (`body-line-too-long`) require `git log`.
## Canonical analogues
Full source depth — in `references/canonical-patterns.md`. Backbone:
| Analog | What we borrow |
| Conventional Commits spec | Header format, type taxonomy, `!` marker, `BREAKING CHANGE:` footer |
| commitlint (conventional-changelog) | Rule set, subject-length/type-enum checks, exit-code gate |
| gitlint | Subject capitalization, trailing-period, body-line-length rules |
| husky + commit-msg hook | Pre-commit validation workflow (we stay a standalone CLI) |
| semantic-release / commitizen | Type taxonomy and the "clean history drives releases" model |
## Installation
```bash
# For opencode
cp -r skills/commit-lint ~/.config/opencode/skills/
# For other agents
# Copy the skill folder to your skills directory; requires Python 3.
# git is optional — --stdin mode works without it.
```
---
> **Note**: the linter validates; it never rewrites. Wire it into the commit
> pipeline as the quality gate before `version-bumper` derives the next
> version — a clean Conventional Commits history is what makes the release
> tooling trustworthy.