bash DevOps October 15, 2025

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.

githooksconventional-commitsautomationci

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 scopefeat(auth): add login
  • Breaking change detectionfeat!: 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