Laptop with code and a small plush octopus.

commitlint: Enforce Conventional Commit Messages Automatically

Git 2026-03-04 · 3 min read commitlint conventional commits git hooks ci/cd automation semantic versioning changelog developer tools
By DevTools Guide Editorial TeamSoftware engineers and developer advocates covering tools, workflows, and productivity for modern development teams.

Inconsistent commit messages are a chronic problem on most projects: "fix stuff", "wip", "update", "asdf". These are useless for understanding history, impossible to parse for changelogs, and unhelpful for code review. commitlint enforces a consistent format automatically — commits that don't conform are rejected before they enter the repository.

Photo by Daniil Komov on Unsplash

Conventional Commits Format

commitlint typically enforces the Conventional Commits specification:

<type>(<scope>): <description>

[optional body]

[optional footer(s)]

Types:

Examples:

feat(auth): add OAuth2 login with Google
fix(api): handle empty response from user service
docs: update README with new configuration options
chore(deps): upgrade TypeScript to 5.4

Install

# Install commitlint and the conventional commits config
npm install --save-dev @commitlint/cli @commitlint/config-conventional

# or with pnpm
pnpm add -D @commitlint/cli @commitlint/config-conventional

Configuration

Create commitlint.config.ts (or .js/.cjs):

import type { UserConfig } from "@commitlint/types";

const config: UserConfig = {
  extends: ["@commitlint/config-conventional"],
  rules: {
    // Customize rules here
    "type-enum": [
      2,           // severity: 2 = error
      "always",    // applicable: always
      ["feat", "fix", "docs", "style", "refactor", "perf", "test", "chore", "ci", "revert"],
    ],
    "scope-case": [2, "always", "lower-case"],
    "subject-max-length": [2, "always", 100],
    "body-max-line-length": [2, "always", 200],
  },
};

export default config;

Rule severity:

Git Hook: lefthook

The most effective way to enforce commitlint is via a git commit-msg hook. With lefthook:

# lefthook.yml
commit-msg:
  commands:
    lint:
      run: npx commitlint --edit {1}
npx lefthook install

Now every commit is validated before it's accepted. Non-conforming commits are rejected with a clear error message.

Git Hook: husky + lint-staged

If you're using husky:

npm install --save-dev husky
npx husky init

Add the commit-msg hook:

echo "npx commitlint --edit \$1" > .husky/commit-msg
chmod +x .husky/commit-msg

Validate a Message Manually

# Test a specific message
echo "feat(api): add pagination support" | npx commitlint

# Test the last commit
npx commitlint --from HEAD~1

CI Integration

Validate commits in CI to catch anything that bypassed local hooks:

# GitHub Actions
- name: Validate commit messages
  run: |
    npx commitlint \
      --from ${{ github.event.pull_request.base.sha }} \
      --to ${{ github.event.pull_request.head.sha }} \
      --verbose

This validates every commit in a PR, not just the latest.

Automatic Changelog Generation

Once commits follow Conventional Commits, you can generate changelogs automatically with conventional-changelog:

npm install --save-dev conventional-changelog-cli

# Generate CHANGELOG.md
npx conventional-changelog -p conventional -i CHANGELOG.md -s

Or with standard-version / release-please:

# standard-version bumps version and generates changelog
npx standard-version

This parses feat commits → MINOR version bump, fix commits → PATCH bump, feat! or BREAKING CHANGE: → MAJOR bump.

Scopes

Scopes make commit messages more precise and enable filtering:

feat(auth): add JWT refresh token rotation
fix(auth): prevent token reuse after logout
feat(billing): add Stripe subscription support
fix(billing): handle declined card gracefully

Define allowed scopes in config:

const config: UserConfig = {
  extends: ["@commitlint/config-conventional"],
  rules: {
    "scope-enum": [
      2,
      "always",
      ["auth", "billing", "api", "ui", "db", "infra", "docs"],
    ],
  },
};

Any scope not in the list is rejected. This enforces consistency across a team.

Breaking Changes

Signal breaking changes two ways:

# Method 1: exclamation mark after type
feat!: remove deprecated v1 API endpoints

# Method 2: BREAKING CHANGE footer
feat(api): restructure user response schema

BREAKING CHANGE: `user.name` field renamed to `user.displayName`

Both trigger a MAJOR version bump in automated versioning.

Custom Rules

const config: UserConfig = {
  extends: ["@commitlint/config-conventional"],
  plugins: ["commitlint-plugin-function-rules"],
  rules: {
    // Require issue reference in footer
    "footer-leading-blank": [1, "always"],

    // Custom: subject must not end with period
    "subject-full-stop": [2, "never", "."],

    // Custom: header must not start with uppercase (type is always lowercase)
    "header-case": [2, "always", "lower-case"],
  },
};

Team Workflow

The typical setup for a team project:

  1. commitlint.config.ts in repo root
  2. lefthook.yml with commit-msg hook
  3. CI job validates all PR commits
  4. release-please or standard-version generates changelogs on merge to main
  5. Semantic versioning is automated

Once in place, commit history becomes a structured data source — not just a narrative. Release notes write themselves.

Common Issues

"Could not find config file": Ensure commitlint.config.ts is in the repo root or specify with --config.

Hook not running: Verify lefthook/husky is installed and hooks are set up: ls .git/hooks/commit-msg.

Squash commits in CI: If you squash PRs, the merge commit needs to conform. Configure your GitHub merge strategy to use a conventional commit for the squash message.

Monorepos: Use scopes for package names: feat(web): ..., fix(api): .... Consider @commitlint/config-nx-scopes for Nx workspaces that auto-infer scopes from package names.

commitlint takes 10 minutes to set up and immediately improves the signal-to-noise ratio in git history. Combined with automated changelogs and semantic versioning, it makes releases significantly less manual.

Get free weekly tips in your inbox. Subscribe to DevTools Guide

More git guides

One focused tutorial every week — no spam, unsubscribe anytime.

Opens Substack to confirm — no spam, unsubscribe anytime.

Before you go...

Get a free weekly guide from DevTools Guide — one focused topic, delivered every week. No spam.

Opens Substack to confirm — no spam, unsubscribe anytime.