Git Hooks with Husky and lint-staged for WordPress Projects

Automating code quality checks at commit time — running PHPCS, ESLint, and Prettier only on the files being staged — prevents style violations and syntax errors from ever reaching the repository. Husky manages the Git hooks and lint-staged runs the linters against staged files only, keeping the pre-commit hook fast even in large WordPress projects.

Problem: A WordPress plugin's JavaScript and PHP code is committed without linting or formatting checks — the codebase accumulates inconsistent indentation, trailing whitespace, and ESLint errors that slip through code review.

Solution: Add Husky to run Git hooks automatically: npx husky init creates a .husky/ directory, and a pre-commit hook runs lint-staged. Configure lint-staged to run eslint --fix on staged JS files and phpcbf on staged PHP files — only changed files are linted, keeping the hook fast. Commit the Husky config to enforce the hooks across the team.


The setup below installs Husky and lint-staged, configures PHPCS and ESLint as pre-commit checks, adds a commit-msg hook to enforce Conventional Commits format, and shows how to skip hooks in emergency situations.


# 1. Install Husky and lint-staged
npm install --save-dev husky lint-staged

# 2. Initialise Husky (creates .husky/ directory and sets core.hooksPath)
npx husky init

# 3. Add pre-commit hook
echo 'npx lint-staged' > .husky/pre-commit

# 4. Add commit-msg hook (Conventional Commits format check)
cat > .husky/commit-msg << 'EOF'
#!/bin/sh
npx --no-install commitlint --edit "$1"
EOF
chmod +x .husky/commit-msg

# 5. Install commitlint
npm install --save-dev @commitlint/cli @commitlint/config-conventional


{
    "lint-staged": {
        "*.php": [
            "composer run phpcs",
            "composer run phpstan --no-interaction -- --no-progress"
        ],
        "*.{js,jsx,ts,tsx}": [
            "eslint --fix",
            "prettier --write"
        ],
        "*.{css,scss}": [
            "stylelint --fix",
            "prettier --write"
        ],
        "*.{json,md}": [
            "prettier --write"
        ]
    },
    "commitlint": {
        "extends": ["@commitlint/config-conventional"]
    }
}


{
    "scripts": {
        "prepare": "husky",
        "phpcs": "vendor/bin/phpcs --standard=WordPress --extensions=php",
        "phpstan": "vendor/bin/phpstan analyse --level=6 src/"
    }
}


# Skip hooks in an emergency (use sparingly)
git commit --no-verify -m "hotfix: revert breaking change"

# Run lint-staged manually without committing
npx lint-staged --verbose


NOTE: lint-staged only runs linters on staged files — files modified but not staged are untouched — so always use git add -p to review your staging area before committing; linting only staged content is intentional and prevents the hook from failing due to unrelated work-in-progress files.