During a recent interview, I had an interesting conversation about coding standards and building pipelines. The company I was speaking with took developer responsibility seriously—they had linters, quality checks, and a dedicated team. But there was something that didn't quite add up.
They were trying to automate everything, yet developers still had to run linters manually before committing code. When I asked why they weren't using pre-commit git hooks, they explained: "Oh, we have pre-commit hooks that run the linter, but developers need to activate them manually after cloning the repository."
That's when it clicked. They had git hooks, but they weren't persistent. Every time someone cloned the repo, they got the default sample hooks instead of the team's carefully crafted automation.
I realized they didn't know about a straightforward configuration that could make their hooks work automatically for everyone immediately after cloning.
The Root Issue
The problem is that Git doesn't track the .git/hooks/
directory. When you clone a repository, you get the default sample hooks - not the team's custom automation. This forces teams into workarounds that defeat the purpose of automation.
The Solution: Custom Hook Paths
Git lets you set a custom location for hooks using the core.hooksPath
setting. Pointing it to a tracked directory in your repository makes your hooks become part of the codebase.
Step 1: Create Your Hook Directory
Create a directory in your repository to store hooks:
mkdir -p scripts/git-hooks
Step 2: Configure Git to Use Your Custom Path
Add this to your repository's .gitconfig
file (create it, if needed):
[core]
hooksPath = ./scripts/git-hooks/
Commit this file to your repository.
Step 3: Create Your Hooks
Here's a simple example of a pre-commit hook that runs PHP linting:
scripts/git-hooks/pre-commit
#!/usr/bin/env bash
REPO=$(git rev-parse --show-toplevel)
# Get list of staged PHP files
FILES=$(git status --porcelain | grep '^[AM] .*\.php$' | cut -c 3- | tr '\n' ' ')
if [ -n "$FILES" ]; then
$REPO/source/vendor/bin/php-cs-fixer --no-interaction --config=$REPO/.quality/php-cs-fixer.php fix $FILES
git add $FILES
fi
Make it executable:
chmod +x scripts/git-hooks/pre-commit
Step 4: Test and Commit
Test your hook:
git add .
git commit -m "Add persistent git hooks"
Advanced Example: PHP Project with Multiple Tools
I developed this solution years ago, and it has since grown significantly. It now supports more than just PHP — it also checks for the availability of required tools and offers to install them if missing.
Don't get me wrong, it is far from perfect. There's still room for improvement. It assumes a Debian-based Linux environment, doesn't run unit tests, and has other limitations. However, it's sufficient for BiStro, the project for which it was initially built.
#!/usr/bin/env bash
REPO=$(git rev-parse --show-toplevel)
export COMPOSER_HOME="$PWD/.composer"
ECS="$REPO/source/vendor/bin/ecs"
ECS_CONFIG="$REPO/.quality/ecs.php"
PHPCS="$REPO/source/vendor/bin/php-cs-fixer"
PHPCS_CONFIG="$REPO/.quality/php-cs-fixer.php"
LATTE="$REPO/source/vendor/bin/latte-lint"
PHPSTAN="$REPO/source/vendor/bin/phpstan"
PHPSTAN_CONFIG="$REPO/.quality/phpstan.neon"
FILES=` git status --porcelain | grep '^[AM] .*\.\(php\|md\)$' | cut -c 3- | tr '\n' ' '`
exec < /dev/tty
function install_tools()
{
sudo apt -qq -y install composer
composer install
composer require --dev symplify/easy-coding-standard
composer require --dev friendsofphp/php-cs-fixer
composer require --dev phpstan/phpstan
}
if [ ! -x $ECS ] || [ ! -x $LATTE ] || [ ! -x $PHPCS ] || [ ! -x $PHPSTAN ]; then
while read -p "No validation tools found, should I install them? (Y/n) " yn; do
case $yn in
[Yy] ) install_tools;;
[Nn] ) exit 1;;
* ) echo "Please answer y (yes) or n (no):" && continue;
esac
done
fi
if [ -x $ECS ] && [ -x $LATTE ] && [ -x $PHPCS ] ; then
if [ -n "$FILES" ]; then
echo "################################## PHP LINTER ##################################"
$ECS check --config=$ECS_CONFIG --fix $FILES
$PHPCS --no-interaction --config=$PHPCS_CONFIG fix $FILES
git add $FILES
fi
echo "################################# LATTE LINTER #################################"
$LATTE $repo_root/source/templates
git add < git status --porcelain | grep '^[MA] .*\.latte$'
echo "################################ STATIC ANALYSIS ###############################"
$PHPSTAN analyse --no-interaction --memory-limit 512M -c $PHPSTAN_CONFIG
fi
exec <&-
Top comments (0)