DEV Community

Karel Křemel
Karel Křemel

Posted on • Originally published at karelkremel.com

Making Git Hooks Persistent - No More Manual Setup

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 
Enter fullscreen mode Exit fullscreen mode

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/ 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Make it executable:

chmod +x scripts/git-hooks/pre-commit 
Enter fullscreen mode Exit fullscreen mode

Step 4: Test and Commit

Test your hook:

git add . git commit -m "Add persistent git hooks" 
Enter fullscreen mode Exit fullscreen mode

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 <&- 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)