Skip to content

Project 05 — Git Hook Collection

Build a set of git hooks that run ShellCheck, check for trailing whitespace, validate commit message format, and block dangerous commits — enforcing quality before code ever leaves your machine.

What You Will Build

A collection of git hooks: - pre-commit — runs ShellCheck on staged .sh files, checks for trailing whitespace, blocks large files - commit-msg — validates commit message format (imperative mood, length limits, ticket ID) - pre-push — runs the full test suite before allowing a push - An install.sh that sets up the hooks in any repository

How Git Hooks Work

Hooks are shell scripts in .git/hooks/. Git runs them automatically at key points. Make them executable (chmod +x) and they run.

.git/hooks/
    pre-commit       ← runs before a commit is created
    commit-msg       ← runs to validate the commit message
    pre-push         ← runs before a push to remote
    post-merge       ← runs after a git merge

The hook's exit code matters: 0 = allow the operation, non-zero = block it.

Getting Started

The pre-commit Hook

#!/usr/bin/env bash
set -euo pipefail

# Run ShellCheck on staged .sh files
staged_sh=$(git diff --cached --name-only --diff-filter=ACM | grep '\.sh$' || true)
if [[ -n "$staged_sh" ]]; then
    echo "Running ShellCheck on staged scripts..."
    echo "$staged_sh" | xargs shellcheck || {
        echo "ShellCheck failed. Fix the issues above before committing."
        exit 1
    }
fi

# Check for trailing whitespace
if git diff --cached --check; then
    :   # no trailing whitespace
else
    echo "ERROR: Staged files contain trailing whitespace."
    echo "Fix with: git diff --cached --check"
    exit 1
fi

# Block files over 1MB
large_files=$(git diff --cached --name-only | xargs -I{} sh -c '[ -f "{}" ] && find "{}" -size +1M -print' 2>/dev/null || true)
if [[ -n "$large_files" ]]; then
    echo "ERROR: Large files staged for commit:"
    echo "$large_files"
    exit 1
fi

The commit-msg Hook

#!/usr/bin/env bash

MSG_FILE="$1"
MSG=$(cat "$MSG_FILE")
FIRST_LINE="${MSG%%$'\n'*}"

# Must be at least 10 characters
if [[ ${#FIRST_LINE} -lt 10 ]]; then
    echo "ERROR: Commit message too short (min 10 chars)"
    exit 1
fi

# Must not exceed 72 characters
if [[ ${#FIRST_LINE} -gt 72 ]]; then
    echo "ERROR: First line too long (max 72 chars, got ${#FIRST_LINE})"
    exit 1
fi

# Must not start with capital letter (enforce lowercase imperative)
if [[ "${FIRST_LINE:0:1}" =~ [A-Z] ]]; then
    echo "WARN: Commit messages should start with a lowercase verb (e.g., 'add', 'fix', 'update')"
fi

install.sh

#!/usr/bin/env bash
set -euo pipefail

HOOKS_DIR="$(git rev-parse --git-dir)/hooks"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

for hook in pre-commit commit-msg pre-push; do
    [-f "$SCRIPT_DIR/$hook"](<../../-f "$SCRIPT_DIR/$hook".md>) || continue
    cp "$SCRIPT_DIR/$hook" "$HOOKS_DIR/$hook"
    chmod +x "$HOOKS_DIR/$hook"
    echo "Installed: $hook"
done
echo "Done. Run 'git commit' to test."

Stretch Goals

  • Add a pre-push hook that runs bats test files if they exist
  • Add branch name validation (e.g., must match feature/, fix/, chore/)
  • Publish the hook collection as a standalone repo with a one-line install command

[[04-backup-script]] | [[06-menu-cli-app]]