Use Git Hooks to Enforce Code Quality Checks Before Commit and Push

Git hooks are shell scripts stored in the .git/hooks/ directory that Git executes automatically at specific points in the version control workflow — pre-commit runs before a commit is finalized, commit-msg validates the commit message format, and pre-push runs before git push sends data to the remote. Because hooks live inside .git/ they are not tracked by Git and not shared with collaborators by default; the standard solution is to store hooks in a versioned directory (e.g., .githooks/) and configure Git to use it with git config core.hooksPath .githooks, or use the husky npm package which manages hook installation automatically via package.json. A pre-commit hook for a WordPress PHP project typically runs PHP CodeSniffer against staged files only: git diff --cached --name-only --diff-filter=ACMR | grep \.php$ | xargs phpcs — running PHPCS only on staged files rather than the entire codebase keeps the hook fast even in large repositories. A commit-msg hook enforces conventional commit format (e.g., feat:, fix:, docs:) by reading $1 (the path to the commit message file) and matching it against a regex — returning exit code 1 aborts the commit and prints a descriptive error. A pre-push hook runs the test suite (phpunit or wp-env run tests-cli vendor/bin/phpunit) and aborts the push if any test fails, preventing broken code from reaching the remote. All hooks must be executable (chmod +x .githooks/pre-commit) and should start with #!/usr/bin/env bash with set -euo pipefail. The hook scripts should provide clear, actionable error messages — a generic “hook failed” message forces developers to re-run the command manually to find the problem. The interactive rebase post and this hooks guide together form a complete Git workflow quality layer: hooks prevent bad commits from entering the history, and interactive rebase cleans up commits before they are pushed to a shared branch.

Problem: Developers commit PHP files with PHPCS errors and push failing tests because there is no automated gate — code style problems and test failures are discovered only after the CI pipeline runs, requiring a fix commit and wasting pipeline minutes.

Solution: Store hooks in a versioned .githooks/ directory, configure Git to use it with core.hooksPath, run PHPCS on staged PHP files in pre-commit, validate commit message format in commit-msg, and run the test suite in pre-push.

#!/usr/bin/env bash
# .githooks/pre-commit — run PHPCS on staged PHP files
set -euo pipefail

STAGED=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.php$' || true)

[[ -z "$STAGED" ]] && exit 0  # no PHP files staged, skip

echo "Running PHP_CodeSniffer on staged files..."
echo "$STAGED" | xargs ./vendor/bin/phpcs --standard=WordPress --colors

echo "PHPCS passed."

#!/usr/bin/env bash
# .githooks/commit-msg — enforce conventional commit format
set -euo pipefail

MSG_FILE="$1"
MSG=$(cat "$MSG_FILE")

# Allow merge commits and revert commits
if echo "$MSG" | grep -qE '^(Merge|Revert) '; then exit 0; fi

PATTERN='^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{1,72}$'

if ! echo "$MSG" | grep -qE "$PATTERN"; then
    echo "ERROR: Commit message does not follow Conventional Commits format."
    echo "Expected: (): "
    echo "Types: feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert"
    echo "Example: fix(checkout): validate coupon before applying discount"
    echo "Your message: $MSG"
    exit 1
fi

#!/usr/bin/env bash
# .githooks/pre-push — run PHPUnit test suite before push
set -euo pipefail

echo "Running PHPUnit test suite before push..."
./vendor/bin/phpunit --colors=always
echo "All tests passed. Pushing..."

# ── Setup: configure Git to use .githooks/ ────────────────────────────
# Run once per clone:
# git config core.hooksPath .githooks
# chmod +x .githooks/*

# Or add to composer.json scripts to auto-configure after composer install:
# "post-install-cmd": ["git config core.hooksPath .githooks", "chmod +x .githooks/*"]

NOTE: Git hooks can be bypassed with git commit --no-verify — they are a developer-experience tool, not a security control. Always back hooks with a CI pipeline (GitHub Actions, GitLab CI, Bitbucket Pipelines) that enforces the same checks on the server side and blocks merges on failure. Hooks catch problems early and reduce feedback loop time; CI enforces them unconditionally.