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.