---
name: commitlint
description: When setting up commit message validation for a project. When project has commitlint.config.js or .commitlintrc files. When configuring CI/CD to enforce commit format. When extracting commit rules for LLM prompt generation. When debugging commit message rejection errors.
enabled: false
source: github:Jamie-BitFlight/claude_skills
imported-from: github:Jamie-BitFlight/claude_skills
---

# Commitlint

Validate commit messages against Conventional Commits format using commitlint configuration and rules.

## When to Use This Skill

Use this skill when:

- Setting up commitlint for a repository
- Configuring commitlint rules and shareable configurations
- Integrating commitlint with pre-commit hooks or CI/CD pipelines
- Extracting rules from commitlint config to generate LLM prompts for commit message generation
- Validating commit messages programmatically
- Troubleshooting commitlint configuration or validation errors
- Understanding commitlint rule syntax and severity levels
- Detecting commitlint configuration files in a repository

## Core Capabilities

### Configuration Detection

Commitlint uses cosmiconfig to find configuration files in this priority order:

**Dedicated config files:**

```text
.commitlintrc
.commitlintrc.json
.commitlintrc.yaml
.commitlintrc.yml
.commitlintrc.js
.commitlintrc.cjs
.commitlintrc.mjs
.commitlintrc.ts
.commitlintrc.cts
commitlint.config.js
commitlint.config.cjs
commitlint.config.mjs
commitlint.config.ts
commitlint.config.cts
```

**Package files:**

- `package.json` with `commitlint` field
- `package.yaml` (PNPM) with `commitlint` field

### Configuration Formats

**JavaScript ES Modules (Recommended):**

```javascript
// commitlint.config.js or commitlint.config.mjs
export default {
  extends: ['@commitlint/config-conventional'],
};
```

**TypeScript:**

```typescript
// commitlint.config.ts
import type { UserConfig } from '@commitlint/types';
import { RuleConfigSeverity } from '@commitlint/types';

const config: UserConfig = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [RuleConfigSeverity.Error, 'always', ['feat', 'fix', 'docs']],
  },
};

export default config;
```

**JSON:**

```json
{
  "extends": ["@commitlint/config-conventional"]
}
```

**YAML:**

```yaml
extends:
  - "@commitlint/config-conventional"
```

### Rule Configuration

Rules are configured as arrays: `[level, applicability, value]`

**Severity Levels:**

| Level | Meaning  | TypeScript Enum               |
| ----- | -------- | ----------------------------- |
| 0     | Disabled | `RuleConfigSeverity.Disabled` |
| 1     | Warning  | `RuleConfigSeverity.Warning`  |
| 2     | Error    | `RuleConfigSeverity.Error`    |

**Applicability:**

- `'always'` - Rule must match
- `'never'` - Rule must not match

**Example rules:**

```javascript
rules: {
  // Error if type is not in enum
  'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert']],

  // Error if type is empty
  'type-empty': [2, 'never'],

  // Error if subject is empty
  'subject-empty': [2, 'never'],

  // Error if header exceeds 100 chars
  'header-max-length': [2, 'always', 100],

  // Error if subject ends with period
  'subject-full-stop': [2, 'never', '.'],

  // Warning if body doesn't have leading blank line
  'body-leading-blank': [1, 'always'],
}
```

## Common Configurations

### @commitlint/config-conventional

The most widely used shareable configuration. Default error-level rules:

| Rule                     | Configuration                                                                                    | Pass Example             | Fail Example        |
| ------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------ | ------------------- |
| `type-enum`              | `['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test']` | `fix: message`           | `foo: message`      |
| `type-case`              | `'lowerCase'`                                                                                    | `fix: message`           | `FIX: message`      |
| `type-empty`             | `never`                                                                                          | `fix: message`           | `: message`         |
| `subject-case`           | `never` + `['sentence-case', 'start-case', 'pascal-case', 'upper-case']`                         | `fix: some message`      | `fix: Some Message` |
| `subject-empty`          | `never`                                                                                          | `fix: message`           | `fix:`              |
| `subject-full-stop`      | `never`, `'.'`                                                                                   | `fix: message`           | `fix: message.`     |
| `header-max-length`      | `100`                                                                                            | Short header             | Header > 100 chars  |
| `body-leading-blank`     | `always` (warning)                                                                               | Blank line before body   | No blank line       |
| `body-max-line-length`   | `100`                                                                                            | Lines <= 100 chars       | Line > 100 chars    |
| `footer-leading-blank`   | `always` (warning)                                                                               | Blank line before footer | No blank line       |
| `footer-max-line-length` | `100`                                                                                            | Lines <= 100 chars       | Line > 100 chars    |

### Complete Configuration Schema

```javascript
// commitlint.config.js
export default {
  // Extend shareable configs (resolved via node resolution)
  extends: ['@commitlint/config-conventional'],

  // Parser preset for parsing commit messages
  parserPreset: 'conventional-changelog-atom',

  // Output formatter
  formatter: '@commitlint/format',

  // Custom rules (override inherited rules)
  rules: {
    'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']],
  },

  // Functions that return true to ignore specific commits
  // Merged with default ignores (merge commits, reverts, semver tags)
  ignores: [(commit) => commit.includes('WIP')],

  // Whether to use default ignore patterns
  // Default patterns: 'Merge pull request', 'Revert X', 'v1.2.3', etc.
  defaultIgnores: true,

  // Custom help URL shown on failure
  helpUrl: 'https://example.com/commit-guidelines',

  // Prompt configuration (for @commitlint/cz-commitlint)
  prompt: {
    messages: {},
    questions: {
      type: {
        description: 'Select the type of change:',
      },
    },
  },
};
```

## CLI Usage

### Installation

```bash
# npm
npm install -D @commitlint/cli @commitlint/config-conventional

# yarn
yarn add -D @commitlint/cli @commitlint/config-conventional

# pnpm
pnpm add -D @commitlint/cli @commitlint/config-conventional
```

### Common Commands

```bash
# Lint the last commit
npx commitlint --last

# Lint a range of commits
npx commitlint --from HEAD~5

# Lint from a specific commit
npx commitlint --from abc1234

# Lint message from stdin
echo "feat: add feature" | npx commitlint

# Lint message from file
npx commitlint < .git/COMMIT_EDITMSG

# Lint with edit flag (reads .git/COMMIT_EDITMSG)
npx commitlint --edit

# Lint with custom config path
npx commitlint --config ./custom-commitlint.config.js

# Print resolved config
npx commitlint --print-config

# Strict mode (warnings become exit code 2)
npx commitlint --last --strict
```

### Exit Codes

| Code | Meaning                            |
| ---- | ---------------------------------- |
| 0    | Success (no errors)                |
| 1    | Lint errors found                  |
| 2    | Warnings found (strict mode only)  |
| 3    | Errors found (strict mode)         |
| 9    | Config file missing (with -g flag) |

## Rule Reference

### Type Rules

| Rule              | Condition                             | Default Applicability    |
| ----------------- | ------------------------------------- | ------------------------ |
| `type-enum`       | `type` is found in value              | `always`                 |
| `type-case`       | `type` is in case `value`             | `always`, `'lower-case'` |
| `type-empty`      | `type` is empty                       | `never`                  |
| `type-max-length` | `type` has `value` or less characters | `always`, `Infinity`     |
| `type-min-length` | `type` has `value` or more characters | `always`, `0`            |

### Subject Rules

| Rule                       | Condition                                | Default Applicability |
| -------------------------- | ---------------------------------------- | --------------------- |
| `subject-case`             | `subject` is in case `value`             | `always`              |
| `subject-empty`            | `subject` is empty                       | `never`               |
| `subject-full-stop`        | `subject` ends with `value`              | `never`, `'.'`        |
| `subject-max-length`       | `subject` has `value` or less characters | `always`, `Infinity`  |
| `subject-min-length`       | `subject` has `value` or more characters | `always`, `0`         |
| `subject-exclamation-mark` | `subject` has exclamation before `:`     | `never`               |

### Scope Rules

| Rule               | Condition                              | Default Applicability    |
| ------------------ | -------------------------------------- | ------------------------ |
| `scope-enum`       | `scope` is found in value              | `always`, `[]`           |
| `scope-case`       | `scope` is in case `value`             | `always`, `'lower-case'` |
| `scope-empty`      | `scope` is empty                       | `never`                  |
| `scope-max-length` | `scope` has `value` or less characters | `always`, `Infinity`     |
| `scope-min-length` | `scope` has `value` or more characters | `always`, `0`            |

### Header Rules

| Rule                | Condition                                   | Default Applicability    |
| ------------------- | ------------------------------------------- | ------------------------ |
| `header-case`       | `header` is in case `value`                 | `always`, `'lower-case'` |
| `header-full-stop`  | `header` ends with `value`                  | `never`, `'.'`           |
| `header-max-length` | `header` has `value` or less characters     | `always`, `72`           |
| `header-min-length` | `header` has `value` or more characters     | `always`, `0`            |
| `header-trim`       | `header` has no leading/trailing whitespace | `always`                 |

### Body Rules

| Rule                   | Condition                                                    | Default Applicability    |
| ---------------------- | ------------------------------------------------------------ | ------------------------ |
| `body-leading-blank`   | `body` begins with blank line                                | `always`                 |
| `body-empty`           | `body` is empty                                              | `never`                  |
| `body-max-length`      | `body` has `value` or less characters                        | `always`, `Infinity`     |
| `body-max-line-length` | `body` lines have `value` or less characters (URLs excluded) | `always`, `Infinity`     |
| `body-min-length`      | `body` has `value` or more characters                        | `always`, `0`            |
| `body-case`            | `body` is in case `value`                                    | `always`, `'lower-case'` |
| `body-full-stop`       | `body` ends with `value`                                     | `never`, `'.'`           |

### Footer Rules

| Rule                     | Condition                                      | Default Applicability |
| ------------------------ | ---------------------------------------------- | --------------------- |
| `footer-leading-blank`   | `footer` begins with blank line                | `always`              |
| `footer-empty`           | `footer` is empty                              | `never`               |
| `footer-max-length`      | `footer` has `value` or less characters        | `always`, `Infinity`  |
| `footer-max-line-length` | `footer` lines have `value` or less characters | `always`, `Infinity`  |
| `footer-min-length`      | `footer` has `value` or more characters        | `always`, `0`         |

### Case Values

For rules that check case (`*-case`):

```javascript
[
  'lower-case',    // lowercase
  'upper-case',    // UPPERCASE
  'camel-case',    // camelCase
  'kebab-case',    // kebab-case
  'pascal-case',   // PascalCase
  'sentence-case', // Sentence case
  'snake-case',    // snake_case
  'start-case',    // Start Case
]
```

## Programmatic Usage

### Load Configuration

```javascript
import load from '@commitlint/load';

async function getCommitlintConfig() {
  const config = await load();
  console.log(config.rules);
  return config;
}
```

### Validate Message

```javascript
import load from '@commitlint/load';
import lint from '@commitlint/lint';

async function validateMessage(message) {
  const config = await load();
  const result = await lint(message, config.rules);

  return {
    valid: result.valid,
    errors: result.errors,
    warnings: result.warnings,
  };
}
```

## LLM Integration Patterns

### Extract Rules for Prompt Generation

Generate LLM-friendly constraints from commitlint config:

```python
def extract_rules_for_prompt(config: dict) -> str:
    """Extract commitlint rules into LLM-friendly format."""
    rules = config.get('rules', {})
    prompt_parts = []

    # Extract type-enum if present
    if 'type-enum' in rules:
        level, applicability, types = rules['type-enum']
        if level > 0 and applicability == 'always':
            prompt_parts.append(f"Allowed commit types: {', '.join(types)}")

    # Extract scope-enum if present
    if 'scope-enum' in rules:
        level, applicability, scopes = rules['scope-enum']
        if level > 0 and applicability == 'always' and scopes:
            prompt_parts.append(f"Allowed scopes: {', '.join(scopes)}")

    # Extract header-max-length
    if 'header-max-length' in rules:
        level, applicability, length = rules['header-max-length']
        if level > 0:
            prompt_parts.append(f"Header must be {length} characters or less")

    # Extract subject-case
    if 'subject-case' in rules:
        level, applicability, cases = rules['subject-case']
        if level > 0 and applicability == 'never':
            prompt_parts.append(f"Subject must NOT use: {', '.join(cases)}")

    return '\n'.join(prompt_parts)
```

### Validation Loop

```python
import subprocess

async def validate_with_commitlint(message: str, cwd: Path | None = None) -> tuple[bool, list[str]]:
    """
    Validate commit message with commitlint.

    Args:
        message: The commit message to validate
        cwd: Working directory (defaults to current directory)

    Returns:
        Tuple of (is_valid, error_messages)
    """
    result = subprocess.run(
        ['npx', 'commitlint'],
        input=message,
        capture_output=True,
        text=True,
        cwd=cwd,
    )

    if result.returncode == 0:
        return True, []

    # Parse errors from stderr
    errors = [line.strip() for line in result.stderr.split('\n') if line.strip()]
    return False, errors
```

**Validation loop pattern:**

1. LLM generates commit message based on diff and rules
2. Run commitlint on generated message
3. If validation fails, feed errors back to LLM with context
4. Retry (max 3 times by default)
5. Return final message (valid or best effort after retries)

## Common Issues

**Node v24 ESM issues:**

Use `.mjs` extension for ES modules config, or add `"type": "module"` to package.json

**Missing extends:**

Config without `extends` or `rules` fails with "Please add rules" error. Include at least one:

```javascript
export default {
  extends: ['@commitlint/config-conventional'],
};
```

**Empty config error:**

Config file must have at least `extends` or `rules` defined.

**Scope enum empty array:**

`scope-enum` with `[]` passes all scopes. Use specific array to restrict:

```javascript
rules: {
  'scope-enum': [2, 'always', ['api', 'ui', 'docs']],
}
```

**Subject case trap:**

`@commitlint/config-conventional` uses `never` with specific cases, meaning those cases are forbidden (not required):

```javascript
// This forbids sentence-case, start-case, pascal-case, upper-case
'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']]
```

## Pre-commit Integration

For pre-commit hook integration with commitlint, activate the pre-commit skill:

```text
Skill(skill: "python3-development:pre-commit")
```

## Conventional Commits Reference

For Conventional Commits format specification and examples, activate the conventional-commits skill:

```text
Skill(skill: "conventional-commits:conventional-commits")
```

## References

### Official Documentation

- [Commitlint Official Site](https://commitlint.js.org/) (accessed 2025-01-15)
- [Configuration Reference](https://commitlint.js.org/reference/configuration.html) (accessed 2025-01-15)
- [Rules Reference](https://commitlint.js.org/reference/rules.html) (accessed 2025-01-15)
- [CLI Reference](https://commitlint.js.org/reference/cli.html) (accessed 2025-01-15)
- [Getting Started Guide](https://commitlint.js.org/guides/getting-started.html) (accessed 2025-01-15)
- [GitHub Repository](https://github.com/conventional-changelog/commitlint) (accessed 2025-01-15)

### Related Specifications

- [Conventional Commits](https://www.conventionalcommits.org/) - Commit message format specification
- [Cosmiconfig](https://github.com/cosmiconfig/cosmiconfig) - Configuration file discovery mechanism

### Source Attribution

This skill was created from reference documentation at `the commit-polish repository` (2025-12-01)
