Conventional Commits

Guide agents to split changes into machine-parseable commit headers: type, optional scope, imperative subject, plus breaking markers and body footers—so changelog, release tooling, and semver rules stay consistent.

The SKILL should list allowed types, scope naming (aligned with packages or module paths), and which types participate in version bumps—so agents do not label doc-only edits as feat.

Pair with PR description and changelog skills: the merge request carries review context; commit headers stay parser-friendly; release notes aggregate from conventional commits.

From draft message to recorded commit

  [ Staging / diff summary ]
        │
        ▼
  ┌─────────────────┐     Pick type; add scope if needed; breaking? → decide on !
  │  Build header     │──── Imperative mood, present tense; often ≤ 72 chars total
  └─────────────────┘
        │
        ▼
  ┌─────────────────┐     Explain why, implementation notes, review hints; blank line after header
  │  Body (optional)  │
  └─────────────────┘
        │
        ▼
  ┌─────────────────┐     BREAKING CHANGE: …; Closes #n; Reviewed-by: (per team)
  │  Footers (opt.)   │
  └─────────────────┘
        │
        ▼
  [ git commit / hook / CI validation passes ]

Parsers care about the first line + footer keywords; the body is still for humans. If you only use ! for breaking behavior, migration notes must live in the body or footers—otherwise release tools may not emit a full BREAKING section.

First-line syntax and length

Basic shape: type(scope)!: subject. Both scope and ! are optional; after the colon there must be a single space, then a short subject.

  • type: lowercase, from the team list; custom types should stay single-token roots without spaces.
  • subject: no trailing period; avoid trailing commas; lowercase first letter (unless your SKILL allows proper nouns).
  • Overlong first lines truncate in terminals and mailing lists; commitlint often defaults to 72—agents can move detail into the body.

Common types and scope

feat and fix most often drive user-visible minor / patch bumps; whether docs, chore, ci, refactor, test, perf, etc. bump versions is tool-configured—document it in the repo.

  • Monorepos: align scope with package or directory conventions (e.g. feat(api):) so changelogs filter cleanly.
  • Do not abuse feat for formatting, comments, or docs-only changes unless the team treats docs as an external “feature”.
  • Link issues in footers with Closes #123 rather than stuffing the subject line.
// commitlint configuration (.commitlintrc.json)
{
  "extends": ["@commitlint/config-conventional"],
  "rules": {
    "type-enum": [2, "always", [
      "feat",     // New feature
      "fix",      // Bug fix
      "docs",     // Documentation only
      "style",    // Formatting, no logic change
      "refactor", // Code change that is neither feat nor fix
      "perf",     // Performance improvement
      "test",     // Adding or updating tests
      "build",    // Build system or dependency changes
      "ci",       // CI configuration changes
      "chore",    // Routine tasks (e.g. version bumps)
      "revert"    // Revert a previous commit
    ]],
    "type-case": [2, "always", "lower-case"],
    "subject-empty": [2, "never"],
    "subject-full-stop": [2, "never", "."],
    "header-max-length": [2, "always", 100],
    "body-leading-blank": [1, "always"],
    "footer-leading-blank": [1, "always"]
  }
}

Breaking changes (! and footers)

Both styles can coexist: feat! or feat(api)!: mark breaking on the first line, or use a dedicated BREAKING CHANGE: footer. Tools like semantic-release usually accept either, but migration notes should still live in footers or body.

When drafting, if public API breakage is uncertain, the SKILL should require a “pending maintainer confirmation” tag or a split commit—avoid silent major bumps.

# Breaking change examples

# Correct: using ! shorthand
feat(api)!: change /users response to return array instead of object

# Correct: using BREAKING CHANGE footer
feat(api): change /users response format

BREAKING CHANGE: /users now returns { data: User[] } instead of User[]
Closes #234

# Correct: both ! and footer (for extra clarity)
feat(auth)!: require email verification before login

BREAKING CHANGE: unauthenticated users without verified email
will receive 403 instead of being auto-verified.
Migration: run scripts/backfill-email-verified.sql

# Anti-pattern: breaking change without annotation
feat(api): update response format   # WRONG - reviewers and tools won't detect the break

Working with squash merge

After squash, history shows one merge commit: agree in the SKILL whether the final squash message comes from PR title + body template, and whether the conventional first line is preserved for automation.

  • If release-please parses PR titles, titles must follow the same type(scope) rules.
  • Multiple small commits on a branch can collapse into one user-facing line in the merge description to avoid duplicate changelog entries.
  • Local commitlint and pre-commit: include typical error output and fix examples in the SKILL so agents can self-correct.
# Husky + commitlint: enforce commit message format pre-commit

# 1. Install dependencies
npm install --save-dev husky @commitlint/cli @commitlint/config-conventional

# 2. Initialize Husky
npx husky init

# 3. Add commit-msg hook
echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg
chmod +x .husky/commit-msg

# 4. Add pre-commit hook (optional: run lint-staged)
echo 'npx lint-staged' > .husky/pre-commit
chmod +x .husky/pre-commit

# 5. Configure lint-staged in package.json
# "lint-staged": {
#   "*.{ts,tsx,js}": ["eslint --fix", "prettier --write"],
#   "*.{css,md}": ["prettier --write"]
# }

# Test: the following commit should be rejected
# git commit -m "updated stuff"  # FAIL: invalid conventional commit format

# This should pass:
# git commit -m "fix(auth): handle null token in refresh flow"

Header validation preview

Assemble a first line below with light validation (lowercase type, balanced scope parens, space after colon, ! placement). Semver hints follow common conventions; actual bumps depend on your release configuration.

Changing type, scope, subject, or the breaking option updates the preview and validation.

Preview


              

---
name: conventional-commits
description: Commit message format, breaking change annotation, and toolchain setup
version: 2.0
---
# Message format
- First line: <type>(<scope>): <subject> (max 100 chars)
- Blank line separator between header and body
- Body: explain WHY not WHAT; wrap at 72 chars
- Footer: BREAKING CHANGE: description, Closes #n

# Common types
- feat: new user-facing feature (bumps MINOR in SemVer)
- fix: bug fix (bumps PATCH)
- perf: performance improvement (bumps PATCH)
- refactor: internal restructuring, no behavior change
- docs / style / test / build / ci / chore: no version bump

# Breaking changes
- Use ! after type/scope: feat(api)!: rename endpoint
- Add BREAKING CHANGE: footer with migration instructions
- Both ! and footer together for maximum clarity

# Toolchain
- commitlint: validates format on every commit
- Husky commit-msg hook: runs commitlint automatically
- Squash merge: PR title becomes the commit, enforce at PR title level

Back to skills More skills