commit-msg Hook
Accha message = acchi documentation. Har commit se pata chale kya hua aur kyun.
The commit-msg hook runs AFTER the user writes their commit message but BEFORE the commit is created.
It receives ONE argument: $1 — the path to a temporary file containing the commit message.
You can READ the message from this file, VALIDATE it, and BLOCK the commit if it doesn't match your rules.
You can also MODIFY the message file in-place — add ticket numbers, normalize formatting, add metadata.
Exit code 0 = accept the message, non-zero = reject and block the commit.
Difference from pre-commit: pre-commit validates CODE, commit-msg validates the MESSAGE.
Difference from prepare-commit-msg: commit-msg runs AFTER user edits, prepare-commit-msg runs BEFORE.
# Create a commit-msg hook
cat > .git/hooks/commit-msg << 'EOF'
#!/bin/bash
# $1 is the path to the commit message file
MSG_FILE=$1
MSG=$(cat "$MSG_FILE")
# Rule 1: Message must not be empty
if [ -z "$MSG" ]; then
echo "❌ Commit message cannot be empty!"
exit 1
fi
# Rule 2: First line must be under 72 characters
FIRST_LINE=$(echo "$MSG" | head -1)
if [ ${#FIRST_LINE} -gt 72 ]; then
echo "❌ First line too long (${#FIRST_LINE} chars). Max 72."
exit 1
fi
# Rule 3: No generic messages
if echo "$MSG" | grep -qE "^(fix|update|changes|wip|done)$"; then
echo "❌ Generic message detected. Be specific!"
echo " Bad: fix"
echo " Good: fix: handle null user in login"
exit 1
fi
exit 0
EOF
chmod +x .git/hooks/commit-msg
# Test it
git commit -m "fix"
# ❌ Generic message detected. Be specific!
git commit -m "fix: handle null user in login flow"
# ✅ Commit createdConventional Commits format: type(scope): description (e.g., feat(auth): add login page).
Types: feat, fix, docs, style, refactor, perf, test, chore, ci.
Enforcing this format makes commit history readable, enables auto-changelogs, and auto-versioning.
The commit-msg hook can validate the first line against this pattern using regex.
Example regex: ^(feat|fix|docs|style|refactor|perf|test|chore|ci)(\(.+\))?: .{1,100}
Breaking changes: feat!: or feat(scope)!: — add this to your pattern.
If you use commitizen or commitlint, they integrate with commit-msg hook automatically.
# Enforce Conventional Commits with commit-msg hook
cat > .git/hooks/commit-msg << 'EOF'
#!/bin/bash
MSG_FILE=$1
MSG=$(cat "$MSG_FILE")
FIRST_LINE=$(echo "$MSG" | head -1)
# Conventional Commits pattern
PATTERN="^(feat|fix|docs|style|refactor|perf|test|chore|ci)(\(.+\))?!?: .{1,100}"
if ! echo "$FIRST_LINE" | grep -qE "$PATTERN"; then
echo "❌ Invalid commit message format!"
echo ""
echo "Expected: type(scope): description"
echo "Types: feat, fix, docs, style, refactor, perf, test, chore, ci"
echo "Scope: optional but recommended (auth, api, ui, etc.)"
echo ""
echo "Examples:"
echo " feat(auth): add JWT login"
echo " fix(api): handle null response"
echo " chore: update dependencies"
echo " feat!: breaking change in API"
echo ""
echo "Your message: $FIRST_LINE"
exit 1
fi
exit 0
EOF
chmod +x .git/hooks/commit-msg
# Test
git commit -m "add login" # ❌ Invalid — no type prefix
git commit -m "feat: add login" # ✅ ValidMany teams require every commit to reference a ticket: JIRA-123, PROJ-456, GH-789.
This links commits to issues, making it easy to trace why a change was made.
commit-msg can check for a ticket pattern in the message.
Strategy 1: BLOCK if no ticket — strict, ensures every commit is linked.
Strategy 2: WARN if no ticket — softer, allows quick fixes without tickets.
You can also auto-extract the ticket from the branch name (covered in prepare-commit-msg chapter).
The ticket can be in the first line or in a footer: Ref: JIRA-123 or Closes #123.
# Require JIRA ticket in commit message
cat > .git/hooks/commit-msg << 'EOF'
#!/bin/bash
MSG_FILE=$1
MSG=$(cat "$MSG_FILE")
# Check for JIRA ticket pattern
if ! echo "$MSG" | grep -qE "[A-Z]+-[0-9]+"; then
echo "❌ Commit must reference a ticket (e.g., JIRA-123)"
echo " Add it to the message or branch name"
exit 1
fi
exit 0
EOF
chmod +x .git/hooks/commit-msg
# Test
git commit -m "fix: handle null user"
# ❌ Commit must reference a ticket (e.g., JIRA-123)
git commit -m "fix: handle null user (JIRA-456)"
# ✅ Commit created
The commit-msg hook can MODIFY the message file — changes you write to $1 become the actual commit message.
Use cases: add ticket number, normalize formatting, add Co-authored-by, strip trailing whitespace.
Be careful: don't accidentally overwrite the user's entire message. Prepend or append, don't replace.
To append: echo "Footer text" >> "$MSG_FILE"
To prepend: write new first line + existing content to temp file, then copy back.
To modify: read content, transform, write back to same file.
# commit-msg that MODIFIES the message
cat > .git/hooks/commit-msg << 'EOF'
#!/bin/bash
MSG_FILE=$1
MSG=$(cat "$MSG_FILE")
# 1. Strip trailing whitespace from each line
sed -i 's/[[:space:]]*$//' "$MSG_FILE"
# 2. Ensure message ends with newline
sed -i -e '$a\' "$MSG_FILE"
# 3. Add Co-authored-by for pair programming
# (Only if not already present)
if ! grep -q "Co-authored-by" "$MSG_FILE"; then
echo "" >> "$MSG_FILE"
echo "Co-authored-by: Pair Partner " >> "$MSG_FILE"
fi
# 4. Validate the message still follows conventions
FIRST_LINE=$(head -1 "$MSG_FILE")
if ! echo "$FIRST_LINE" | grep -qE "^(feat|fix|chore|docs)"; then
echo "❌ Message must start with feat/fix/chore/docs"
exit 1
fi
exit 0
EOF
chmod +x .git/hooks/commit-msg
# When developer commits:
git commit -m "feat: add login " # trailing spaces
# Hook strips trailing spaces, adds Co-authored-by
# Final message:
# feat: add login
#
# Co-authored-by: Pair Partner Writing commit-msg hooks in bash is fine for simple rules, but commitlint is the industry standard.
commitlint: a Node.js tool that validates commit messages against configurable rules.
@commitlint/config-conventional: preset for Conventional Commits.
Integrates with Husky: npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
Rules are configurable: max length, required scope, allowed types, case rules, etc.
commitlint.config.js: module.exports = { extends: ['@commitlint/config-conventional'] };
Can be strict or lenient — configure based on your team's needs.
# Install commitlint with conventional config
npm install --save-dev @commitlint/cli @commitlint/config-conventional
# Create config
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
# Add to Husky
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
# Test it
git commit -m "update code"
# ⧗ input: update code
# ✖ subject may not be empty [subject-empty]
# ✖ type may not be empty [type-empty]
git commit -m "feat: add login"
# ✔ passes all rules
Lo kar liya — Key Points:
- ✅ commit-msg hook runs AFTER user writes the message but BEFORE the commit is created
- ✅ It receives one argument $1 — the path to the file containing the commit message
- ✅ Exit 0 = accept the message, non-zero = reject and block the commit
- ✅ Enforce Conventional Commits format: type(scope): description — enables auto-changelogs and versioning
- ✅ The hook can MODIFY the message file — append ticket numbers, add Co-authored-by, strip whitespace
- ✅ Don't make rules too strict — developers will use --no-verify if every commit requires a perfect message
- ✅ For JS projects, commitlint + Husky is the standard — don't reinvent bash scripts
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login