Git hooks are scripts that run automatically on Git operations. A pre-commit hook run before every commit eliminates a class of errors before they enter history. Commit-msg enforces message conventions. Pre-push protects the remote branch from broken code. I show how to configure this in a Magento 2 project with DDEV.
Hook types
| Hook | When | Typical use |
|---|---|---|
| pre-commit | Before saving the commit | phpcs, phpstan, check for debuggers |
| commit-msg | After entering the message | Validate Conventional Commits format |
| pre-push | Before sending to remote | Full unit test suite |
| post-merge | After merge/pull | composer install, cache flush |
pre-commit – phpcs and phpstan
#!/bin/bash
# .git/hooks/pre-commit
set -e
echo "Running PHP CS Fixer..."
files=$(git diff --cached --name-only --diff-filter=ACMR | grep \.php$)
if [ -n "$files" ]; then
vendor/bin/phpcs --standard=PSR12 $files
fi
echo "Running PHPStan..."
if [ -n "$files" ]; then
vendor/bin/phpstan analyse $files --level=8 --no-progress
fi
# Check for var_dump or dd()
if git diff --cached | grep -E "^\+.*var_dump|^\+.*dd\("; then
echo "ERROR: var_dump() or dd() found in staged files"
exit 1
fi
echo "Pre-commit checks passed!"
commit-msg – format validation
#!/bin/bash
# .git/hooks/commit-msg
COMMIT_MSG=$(cat "$1")
PATTERN="^(feat|fix|refactor|docs|test|chore|perf)(\([a-z-]+\))?: .{10,72}"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "ERROR: Commit message format invalid."
echo "Required: type(scope): description (10-72 chars)"
echo "Example: feat(catalog): add product export service"
exit 1
fi
pre-push – tests before sending
#!/bin/bash
# .git/hooks/pre-push
BRANCH=$(git symbolic-ref HEAD | sed 's/refs\/heads\///')
# Only for main and develop
if [[ "$BRANCH" == "main" || "$BRANCH" == "develop" ]]; then
echo "Running full test suite before push to $BRANCH..."
vendor/bin/phpunit --testsuite=Unit
fi
Sharing hooks across the team
The .git/hooks/ directory is not version-controlled. Best practice: store hooks in .githooks/ and configure the path.
# Configure hooks directory for the whole project git config core.hooksPath .githooks # Or add to composer.json post-install-cmd: # "git config core.hooksPath .githooks" # "chmod +x .githooks/*"
DDEV integration
#!/bin/bash # .githooks/pre-commit - DDEV version set -e files=$(git diff --cached --name-only --diff-filter=ACMR | grep \.php$) [ -z "$files" ] && exit 0 # Run phpcs and phpstan inside the DDEV container ddev exec vendor/bin/phpcs --standard=PSR12 $files ddev exec vendor/bin/phpstan analyse $files --level=8 --no-progress echo "All checks passed!"
Summary
Git hooks are the first line of defence against errors in history. Pre-commit with phpcs and phpstan eliminates code quality issues before they reach the repo. Commit-msg enforces readable messages. Storing hooks in .githooks/ and configuring via git config core.hooksPath shares them with the whole team. Next post: debugging and rescue – bisect, reflog, reset vs revert.
