Enforce Commit Standards with Commitlint and Husky commit-msg Hook
Your team's Git history tells a story. But is it a coherent one?
Look at these real commit messages from a typical project:
"fixed bug""Updated stuff""asdfasdf""PLEASE WORK THIS TIME""Fixed the fix that fixed the original fix"
Now imagine trying to:
- Generate a changelog from these messages
- Understand what changed between releases
- Find when a specific feature was added
- Track down which commit introduced a bug
Impossible.
This is where Conventional Commits and commitlint save your team. In this guide, you'll learn how to automatically enforce consistent, meaningful commit messages using commitlint with Husky's commit-msg hook.
The Team Collaboration Problem
When multiple developers work on the same repository without commit standards:
❌ Meaningless commit messages - "fix", "update", "wip"
❌ Inconsistent formatting - Everyone writes commits differently
❌ Hard to track changes - Can't tell what changed without reading code
❌ No automated changelogs - Can't generate release notes automatically
❌ Difficult debugging - Can't find which commit introduced issues
❌ Poor documentation - Git history doesn't tell the project story
The Solution: Conventional Commits + commitlint
We'll set up a system that:
✅ Enforces consistent format - Same structure for every commit
✅ Runs automatically - Validates on every commit attempt
✅ Provides clear feedback - Tells developers exactly what's wrong
✅ Enables automation - Changelogs, versioning, releases
✅ Improves collaboration - Everyone understands the history
The setup uses Conventional Commits (a specification), commitlint (validation tool), and Husky (Git hooks) to create a commit-msg hook that validates every commit message before it's created.
Prerequisites
This guide assumes you already have:
- A project with Husky installed (see our pre-commit hooks guide)
- Git initialized in your repository
If you don't have Husky yet, install it first:
1npm install --save-dev husky
2npx husky initWhat are Conventional Commits?
Conventional Commits is a specification for commit messages with this structure:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Examples:
feat: add user authentication
fix: resolve login button styling issue
docs: update README with setup instructions
refactor(api): simplify error handling logic
feat(auth)!: change password requirements
Common Types
feat- A new featurefix- A bug fixdocs- Documentation changesstyle- Code style changes (formatting, semicolons, etc.)refactor- Code refactoring (neither fixes bug nor adds feature)perf- Performance improvementstest- Adding or updating testschore- Maintenance tasks (dependencies, config, etc.)ci- CI/CD changesbuild- Build system changes
Why This Matters for Teams
For Developers:
- Clear structure reduces decision fatigue
- Easy to write once you know the pattern
- Better context when reviewing old commits
For Code Reviewers:
- Quickly understand commit purpose from message alone
- Easier to trace features and fixes
- Better pull request organization
For Project Managers:
- Automated changelog generation
- Track feature delivery
- Better release planning
For Everyone:
- Searchable commit history (
git log --grep="feat:") - Automated semantic versioning
- Professional, maintainable codebase
Learn more about Conventional Commits
Step 1: Install commitlint
Install commitlint and the conventional config:
1npm install --save-dev @commitlint/cli @commitlint/config-conventionalWhat these packages do:
@commitlint/cli- The commitlint command-line tool@commitlint/config-conventional- Conventional Commits ruleset
Step 2: Configure commitlint
Create a commitlint.config.js file in your project root:
1module.exports = {
2 extends: ['@commitlint/config-conventional'],
3 rules: {
4 'type-enum': [
5 2,
6 'always',
7 [
8 'feat',
9 'fix',
10 'docs',
11 'style',
12 'refactor',
13 'perf',
14 'test',
15 'chore',
16 'ci',
17 'build',
18 'revert'
19 ]
20 ],
21 'subject-case': [2, 'never', ['upper-case', 'pascal-case', 'start-case']],
22 'subject-empty': [2, 'never'],
23 'subject-full-stop': [2, 'never', '.'],
24 'type-empty': [2, 'never'],
25 }
26};Team benefit: This configuration becomes your team's commit message standard. Everyone follows the same rules, enforced automatically.
Alternative (JSON format):
Create .commitlintrc.json:
1{
2 "extends": ["@commitlint/config-conventional"]
3}The JavaScript version allows more customization, but the JSON version works fine for most teams.
Learn more about commitlint configuration
Step 3: Add Husky commit-msg Hook
Create the commit-msg hook:
1npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'This creates a .husky/commit-msg file with:
1npx --no -- commitlint --edit $1What this does:
- Husky intercepts every commit attempt
- Runs commitlint on the commit message
$1is the file containing the commit message- If validation fails, the commit is rejected
Team benefit: Once this hook is committed to the repository, every team member automatically gets commit message validation after running npm install. No manual setup required.
Step 4: Commit Your Setup
Save your commitlint configuration:
1git add .
2git commit -m "chore: add commitlint with conventional commits"Team milestone: Push this to your repository. From now on, every team member must follow Conventional Commits. No exceptions.
Step 5: Test Your commit-msg Hook
Let's test both valid and invalid commit messages.
❌ Test Invalid Commit
Try committing with a bad message:
1git add .
2git commit -m "fixed bug"Result: Commit is rejected with an error:
⧗ input: fixed bug
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]
✖ found 2 problems, 0 warnings
ⓘ Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint
✅ Test Valid Commit
Now try with a proper format:
1git add .
2git commit -m "fix: resolve login button bug"Result: Commit succeeds! ✅
More Examples
Invalid commits (will be rejected):
1git commit -m "Fixed the bug" # No type
2git commit -m "FEAT: add feature" # Subject can't start with uppercase
3git commit -m "feat:" # Empty subject
4git commit -m "feat: Add new feature." # No period at end
5git commit -m "update: changed stuff" # 'update' is not a valid typeValid commits (will succeed):
1git commit -m "feat: add user dashboard"
2git commit -m "fix: resolve memory leak in data fetching"
3git commit -m "docs: update API documentation"
4git commit -m "refactor: simplify authentication logic"
5git commit -m "test: add unit tests for user service"
6git commit -m "chore: update dependencies"
7git commit -m "feat(auth): add two-factor authentication"
8git commit -m "fix(api)!: change response format"How It Works
Here's the workflow that happens for every commit:
- Developer writes code and stages changes
- Developer runs
git commit -m "message" - Husky intercepts the commit
- The
commit-msghook triggers - commitlint validates the message format
- If valid → commit succeeds
- If invalid → commit is blocked with clear error message
Result: Only well-formatted commit messages make it into your repository.
The Team Workflow in Action
New Developer Joins
- Clone the repository
- Run
npm install - Husky hooks automatically installed
- First commit attempt teaches them the format
- They quickly learn Conventional Commits
Developer Makes a Commit
- Write code and stage changes
- Try to commit:
git commit -m "fixed bug" - Commit rejected with helpful error
- Fix format:
git commit -m "fix: resolve authentication bug" - Commit succeeds
Automated Benefits
Your team now gets:
- Automated changelogs using tools like standard-version or semantic-release
- Semantic versioning based on commit types (feat = minor, fix = patch)
- Better git log that's actually readable and searchable
- Clear project history anyone can understand
Advanced: Commit Message Templates
Help your team by providing a commit message template.
Create .gitmessage in your project root:
# <type>(<scope>): <subject>
# │ │ │
# │ │ └─⫸ Summary in present tense. Not capitalized. No period.
# │ │
# │ └─⫸ Commit Scope: api|ui|auth|db|etc
# │
# └─⫸ Commit Type: feat|fix|docs|style|refactor|perf|test|chore|ci|build
#
# Examples:
# feat: add email notifications
# fix: resolve navigation bug
# docs: update README
# feat(api): add user search endpoint
# fix(ui)!: change button styling (breaking change)
Configure git to use this template:
1git config commit.template .gitmessageTeam benefit: When developers run git commit (without -m), they see this helpful template in their editor.
Customizing Rules for Your Team
You can customize commitlint rules in commitlint.config.js:
1module.exports = {
2 extends: ['@commitlint/config-conventional'],
3 rules: {
4 // Customize maximum subject length
5 'subject-max-length': [2, 'always', 72],
6
7 // Add custom types
8 'type-enum': [
9 2,
10 'always',
11 [
12 'feat',
13 'fix',
14 'docs',
15 'style',
16 'refactor',
17 'perf',
18 'test',
19 'chore',
20 'ci',
21 'build',
22 'revert',
23 'wip' // Custom type for work in progress
24 ]
25 ],
26
27 // Require scope for certain types
28 'scope-empty': [2, 'never'],
29
30 // Custom scope values
31 'scope-enum': [
32 2,
33 'always',
34 ['api', 'ui', 'auth', 'db', 'config']
35 ]
36 }
37};Team consideration: Discuss and agree on customizations as a team. Don't make rules too strict or developers will find them frustrating.
Combining with Pre-commit Hooks
You now have two hooks working together:
Pre-commit hook (from previous guide):
- Formats code with Prettier
- Lints code with ESLint
- Runs on staged files
Commit-msg hook (this guide):
- Validates commit message format
- Enforces Conventional Commits
- Runs on commit message
Together, they ensure:
- ✅ Code is properly formatted
- ✅ Code passes linting
- ✅ Commit message follows standards
Your team now has a complete quality gate for every commit!
Documentation for Your Team
Add this to your README.md:
1## Commit Message Guidelines
2
3This project follows [Conventional Commits](https://www.conventionalcommits.org/).
4
5### Format<type>(<scope>): <description>
### Types
- **feat**: New feature
- **fix**: Bug fix
- **docs**: Documentation changes
- **style**: Code style changes
- **refactor**: Code refactoring
- **test**: Adding or updating tests
- **chore**: Maintenance tasks
### Examples
feat: add user authentication fix: resolve login button styling docs: update README with setup instructions
Commits are automatically validated. Invalid formats will be rejected.
What's Next?
This guide covered commit-msg hooks for enforcing commit standards. But there's more you can automate:
- Pre-push hooks - Run tests before pushing to prevent broken code on remote
- Branch naming validation - Enforce consistent branch names like
feature/user-auth - Automated changelog generation - Use standard-version or semantic-release
- Semantic versioning - Automatically bump versions based on commit types
Stay tuned for upcoming guides on these team collaboration topics!
Summary
You've successfully set up commit message validation:
✅ commitlint - Validates commit message format
✅ Conventional Commits - Consistent commit structure
✅ Husky commit-msg hook - Automatic enforcement
✅ Team standards - Everyone follows the same rules
Your team's Git history is now clean, searchable, and professional.
Key Team Benefits
Before commitlint:
"fixed bug""update""asdfasdf""PLEASE WORK"
After commitlint:
feat: add user authenticationfix: resolve memory leak in data fetchingdocs: update API documentationrefactor: simplify error handling
The difference?
- ✅ Clear, professional commit history
- ✅ Automated changelog generation
- ✅ Easy to search and understand
- ✅ Better team collaboration
- ✅ Semantic versioning support
References
- Conventional Commits Specification
- commitlint Documentation
- Husky Documentation
- standard-version - Automated changelog and versioning
- semantic-release - Fully automated release workflow
Building a professional development team? Share this guide! Got questions about commit standards? Drop a comment below. 🚀
