commitlint
Overview
commitlint (@commitlint/cli, v21 at the time of writing) lints a commit message against a rule set; the usual rule set is @commitlint/config-conventional, which enforces type(scope?): subject. It requires Node.js 22.12 or newer. It runs in a Git commit-msg hook (typically via husky) and in CI. Valid messages let tools such as semantic-release, release-please, changesets or conventional-changelog derive version bumps and changelogs.
Instructions
Step 1: Install and add the hook (husky v9)
npm install -D @commitlint/cli @commitlint/config-conventional husky
npx husky init
echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg
npx husky init also creates .husky/pre-commit containing npm test, which makes every commit fail in a project without tests. Edit or empty that file if you do not want it.
Step 2: Configure
Create commitlint.config.mjs (or commitlint.config.js with "type": "module", .cjs, .ts, .commitlintrc.*, or a commitlint key in package.json).
// commitlint.config.mjs
export default {
extends: ['@commitlint/config-conventional'],
rules: {
// [severity, applicable, value]; severity 0 off, 1 warning, 2 error
'scope-enum': [2, 'always', ['auth', 'api', 'billing', 'ui', 'deps']],
'scope-case': [2, 'always', 'kebab-case'],
'header-max-length': [2, 'always', 72],
},
}
The default type-enum is build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test. If you override type-enum, repeat the types you still want, including revert. Default config-conventional limits headers and body/footer lines to 100 characters and forbids sentence-case subjects and trailing periods.
Step 3: Try it
echo "updated stuff" | npx commitlint # exit 1: subject and type may not be empty
echo "feat(auth): add Google OAuth login" | npx commitlint # exit 0
git commit -m "fix(api): handle null response from /users" # hook runs commitlint
Step 4: Lint in CI
# .github/workflows/commitlint.yml
name: commitlint
on: [pull_request]
jobs:
commitlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
Other useful forms: npx commitlint --last (latest commit), --from origin/main (everything since main), --config path for a non-default config. If you squash-merge, lint the PR title instead of the individual commits.
Examples
Example 1: Add commit linting to an existing repo
User request: "Make everyone on the team use conventional commits."
Run Step 1 and Step 2, empty .husky/pre-commit if the repo has no npm test, and commit commitlint.config.mjs and .husky/. Then git commit -m "updated stuff" is rejected with type may not be empty [type-empty], and git commit -m "feat(billing): add annual plan" succeeds. Add a prepare script ("prepare": "husky", which husky init sets) so teammates get hooks after npm install.
Example 2: Restrict scopes to the monorepo packages
User request: "Only allow scopes that match our package names."
Set 'scope-enum': [2, 'always', ['web', 'api', 'worker']]. Then echo "feat(mobile): add push" | npx commitlint exits 1 with scope must be one of [web, api, worker] [scope-enum], while fix(api): retry on 503 passes.
Guidelines
- Hooks can be bypassed with
git commit --no-verify; keep the CI check as the enforcement point.
- Merge and revert commits generated by Git are ignored by default; do not disable that.
subject-max-length does not exist in config-conventional by default; the header limit is header-max-length (100).
- Breaking changes use
feat!: or a BREAKING CHANGE: footer and map to a major version; feat maps to minor, fix to patch.
- commitlint only lints messages; it does not generate changelogs. Pair it with a release tool.
- On older Node versions install an older commitlint major; v21 needs Node 22.12 or newer.