Automate code quality checks in WordPress projects with Git hooks

Git hooks are shell scripts that Git executes automatically at specific points in the version control workflow — before a commit, after a push, before a merge, and so on. For WordPress development the most valuable hook is pre-commit, which runs before Git finalises a commit and can abort it if the script exits with a non-zero code. This makes it the perfect place to enforce code quality rules: run a PHP syntax check on every staged PHP file, run PHP_CodeSniffer with the WordPress Coding Standards ruleset to catch style violations, run ESLint on staged JavaScript files, and check for debug artifacts like var_dump(), console.log(), or hardcoded credentials. The hook lives at .git/hooks/pre-commit and is a plain shell script. Because .git/ is not tracked by Git, hooks are not shared with team members by default — the standard solution is to store hooks in a .githooks/ directory at the project root (which is tracked) and configure Git to use that directory with git config core.hooksPath .githooks, or use a tool like Husky for JavaScript projects. PHP_CodeSniffer with the WordPress standard checks for correct hook naming, proper sanitisation and escaping, nonce usage, and dozens of other WordPress-specific rules from the WordPress Coding Standards repository. Running these checks at commit time rather than in CI means developers get instant feedback and bad code never reaches the repository. Pair this with the rebase and squash guide and the git stash guide for a complete Git workflow.

Problem: PHP syntax errors, coding standard violations, and debug artifacts like var_dump() are committed to the repository and discovered only during code review or in production.

Solution: Create a pre-commit hook that checks only staged files and aborts the commit if any check fails:

#!/usr/bin/env bash
# .githooks/pre-commit
# Run: git config core.hooksPath .githooks
# Make executable: chmod +x .githooks/pre-commit

set -e

STAGED_PHP=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$' || true)
STAGED_JS=$(git diff  --cached --name-only --diff-filter=ACM | grep '\.js$'  || true)

# 1. PHP syntax check on every staged PHP file
if [ -n "$STAGED_PHP" ]; then
    echo "Running PHP syntax check..."
    for file in $STAGED_PHP; do
        php -l "$file" > /dev/null || { echo "Syntax error in $file"; exit 1; }
    done
fi

# 2. PHPCS with WordPress Coding Standards
if [ -n "$STAGED_PHP" ] && command -v phpcs &>/dev/null; then
    echo "Running PHPCS (WordPress standards)..."
    echo "$STAGED_PHP" | xargs phpcs --standard=WordPress --warning-severity=0 || exit 1
fi

# 3. ESLint on staged JS files
if [ -n "$STAGED_JS" ] && command -v eslint &>/dev/null; then
    echo "Running ESLint..."
    echo "$STAGED_JS" | xargs eslint || exit 1
fi

# 4. Block debug artifacts
if [ -n "$STAGED_PHP" ]; then
    ARTIFACTS=$(echo "$STAGED_PHP" | xargs grep -l 'var_dump\|print_r\|error_log\|dd(' 2>/dev/null || true)
    if [ -n "$ARTIFACTS" ]; then
        echo "ERROR: Debug artifacts found in:"
        echo "$ARTIFACTS"
        exit 1
    fi
fi

echo "Pre-commit checks passed."

# Install PHPCS and WordPress Coding Standards via Composer
composer require --dev squizlabs/php_codesniffer wp-coding-standards/wpcs

# Register the WordPress standard with PHPCS
./vendor/bin/phpcs --config-set installed_paths vendor/wp-coding-standards/wpcs

# Verify
./vendor/bin/phpcs -i
# Should include: WordPress, WordPress-Core, WordPress-Docs, WordPress-Extra

# Activate the shared hooks directory for all developers
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit

NOTE: If PHPCS is not installed globally, update the hook to use the local Composer binary: replace phpcs with ./vendor/bin/phpcs. To bypass the hook in exceptional cases (e.g., a work-in-progress commit), use git commit --no-verify — but this should be rare and intentional. Add the .githooks/pre-commit script and the composer.json to version control so every developer on the team gets the same checks automatically after running git config core.hooksPath .githooks.