Git Hook for Conventional Commits
A commit-msg Git hook that enforces the Conventional Commits specification — validates type, scope, and message format with clear error messages.
Description
A zero-dependency Bash script that validates commit messages against the Conventional Commits specification. Install it as a commit-msg hook to enforce consistent commit history.
Features
- Validates commit type — feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
- Optional scope —
feat(auth): add login - Breaking change detection —
feat!: remove legacy API - Subject line length — max 72 characters
- Clear error messages with examples
- No dependencies — pure Bash
The Hook
#!/bin/bash
# .husky/commit-msg (or .git/hooks/commit-msg)
# Validates commit messages follow Conventional Commits spec
COMMIT_MSG_FILE="$1"
COMMIT_MSG=$(head -1 "$COMMIT_MSG_FILE")
# Allow merge commits and rebases
if echo "$COMMIT_MSG" | grep -qE "^(Merge|Revert|fixup!|squash!)"; then
exit 0
fi
# Conventional Commits pattern
# type(optional-scope)!: description
PATTERN="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9 _-]+\))?(!)?: .{1,}"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo ""
echo "❌ Invalid commit message format!"
echo ""
echo " Your message: $COMMIT_MSG"
echo ""
echo " Expected format: <type>(<scope>): <description>"
echo ""
echo " Valid types:"
echo " feat — A new feature"
echo " fix — A bug fix"
echo " docs — Documentation only"
echo " style — Formatting, semicolons, etc."
echo " refactor — Code change that neither fixes nor adds"
echo " perf — Performance improvement"
echo " test — Adding or fixing tests"
echo " build — Build system or dependencies"
echo " ci — CI/CD configuration"
echo " chore — Other changes (no src or test)"
echo " revert — Revert a previous commit"
echo ""
echo " Examples:"
echo " feat: add user registration"
echo " fix(auth): resolve token refresh loop"
echo " docs: update API readme"
echo " refactor!: drop Node 16 support"
echo ""
exit 1
fi
# Check subject line length
SUBJECT_LENGTH=${#COMMIT_MSG}
MAX_LENGTH=72
if [ "$SUBJECT_LENGTH" -gt "$MAX_LENGTH" ]; then
echo ""
echo "⚠️ Commit subject too long!"
echo " Length: $SUBJECT_LENGTH characters (max: $MAX_LENGTH)"
echo " Please shorten your commit message."
echo ""
exit 1
fi
# Check description is not empty after type
DESC=$(echo "$COMMIT_MSG" | sed -E 's/^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?(!)?: //')
if [ -z "$(echo "$DESC" | xargs)" ]; then
echo ""
echo "❌ Empty commit description!"
echo " Add a meaningful description after the type prefix."
echo ""
exit 1
fi
# Check description starts with lowercase
FIRST_CHAR=$(echo "$DESC" | cut -c1)
if echo "$FIRST_CHAR" | grep -qE "^[A-Z]$"; then
echo ""
echo "⚠️ Description should start with lowercase."
echo " Got: $COMMIT_MSG"
echo " Use: $(echo "$COMMIT_MSG" | sed "s/$DESC/$(echo "$DESC" | sed 's/^./\L&/')/") "
echo ""
exit 1
fi
exit 0
Installation
# With Husky
cp conventional-commits-hook.sh .husky/commit-msg
chmod +x .husky/commit-msg
# Without Husky (manual)
cp conventional-commits-hook.sh .git/hooks/commit-msg
chmod +x .git/hooks/commit-msg
Usage
git commit -m "feat: add dark mode toggle" # ✅ Pass
git commit -m "fix(api): handle 404 response" # ✅ Pass
git commit -m "refactor!: drop legacy support" # ✅ Pass (breaking change)
git commit -m "updated stuff" # ❌ Fail — no type prefix
git commit -m "feat: " # ❌ Fail — empty description
git commit -m "feat: Add login page" # ❌ Fail — uppercase description