
September 10, 2026
13 min read
By Kokil Thapa | Last reviewed: September 2026
Git Hooks: Automate Checks Before Commit and Push is how you stop bad commits at the keyboard instead of in production. A hook is a script Git runs at defined lifecycle points — before a commit lands, before a push leaves your machine, after a merge completes. On real client projects I maintain with custom Laravel and PHP applications, hooks catch formatting drift, missing migrations, and accidental .env commits long before CI burns minutes. This guide covers native hooks, shared team setups, and the tools that make enforcement painless in 2026.
pre-commit for lint and format, commit-msg for message rules, and pre-push for tests so broken code never reaches the remote.How do Git hooks work before commit and push?
Git hooks are executable scripts stored inside .git/hooks/. Git invokes them automatically when you run commands like git commit or git push. A hook receives context through environment variables and stdin. It exits with code 0 to allow the action, or any non-zero code to block it.
The hook lifecycle splits into client-side and server-side events. Client hooks run on your machine. Server hooks run on the remote when someone pushes. For day-to-day developer workflow, client hooks matter most.
Client-side hooks that matter most
These hooks appear in almost every production workflow I set up:
- pre-commit — runs after
git commitis invoked but before the commit object is created. Ideal for fast checks: PHP syntax, Pint, ESLint on staged files, secret scanning. - prepare-commit-msg — modifies the default commit message. Useful for prepending ticket IDs like
JIRA-123. - commit-msg — validates the final message. Enforces Conventional Commits or minimum length rules.
- pre-push — runs before refs update on the remote. Run PHPUnit, Pest, or npm test here because push-time checks can take longer.
- post-merge — runs after a successful merge. Common pattern: run
composer installwhencomposer.lockchanged.
Git ships sample hooks as .sample files inside .git/hooks/. Copy one, remove the extension, make it executable, and Git picks it up immediately. No restart required.
Create your first pre-commit hook manually
Start with a minimal PHP syntax check. This works on any Laravel 12 or Symfony project without extra dependencies:
#!/bin/sh
# .git/hooks/pre-commit
STAGED_PHP=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$')
if [ -z "$STAGED_PHP" ]; then
exit 0
fi
for FILE in $STAGED_PHP; do
php -l "$FILE" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Syntax error in $FILE — commit blocked."
exit 1
fi
done
exit 0 Make it executable with chmod +x .git/hooks/pre-commit. Stage a file with a syntax error and try committing. Git blocks the commit and prints your message. Fix the file, stage again, and the hook passes.
A common mistake is putting slow integration tests in pre-commit. Developers bypass hooks with git commit --no-verify when feedback takes more than 10–15 seconds. Keep pre-commit fast. Push heavier work to pre-push or CI.
What are the most useful Git hooks for Laravel and PHP projects?
On production Laravel applications I maintain, hooks enforce the same standards CI checks later. The difference is timing — you learn about the problem while context is fresh. PHP 8.3 or 8.5, Laravel 13.x or 12.x, Composer 2.10 — the toolchain is consistent across most of my deployments.
Recommended checks by hook type
- pre-commit: Laravel Pint (
vendor/bin/pint --test), PHPStan or Larastan on changed files, block commits containing.env,storage/logs/, or private keys. - commit-msg: Require format like
feat(booking): add deposit payment flow— matches patterns from trunk-based and GitFlow branching. - pre-push: Run
php artisan testor Pest suite, verifycomposer validate, optionally run frontend build with Vite 8.x if assets changed. - post-merge: Auto-run
composer install --no-interactionwhen lock file changes, clear config cache on shared EC2 deploy targets.
For WordPress 7.1 or WooCommerce 11.1 theme projects, swap Pint for PHPCS with WordPress coding standards. The hook structure stays identical.
Block secrets before they enter history
Scanning for API keys and database passwords belongs in every hook setup. I've seen .env fragments committed because a developer force-added a config override. A five-line grep check prevents hours of credential rotation.
#!/bin/sh
# Secret scan snippet for pre-commit
FORBIDDEN='\.env$|id_rsa|\.pem$|credentials\.json'
STAGED=$(git diff --cached --name-only)
echo "$STAGED" | grep -E "$FORBIDDEN" && {
echo "Blocked: sensitive file detected in staged changes."
exit 1
}
exit 0 For deeper scanning, integrate Gitleaks in Git and CI pipelines. Run Gitleaks in pre-commit on staged diffs and again in GitLab CI for defence in depth.
Laravel Pint in a pre-commit hook
Laravel Pint ships with Laravel 12 and 13. A practical pre-commit wrapper runs Pint only on staged PHP files:
#!/bin/sh
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$')
[ -z "$FILES" ] && exit 0
echo "$FILES" | xargs vendor/bin/pint
git add $FILES
exit 0 This auto-fixes style and re-stages files. Your commit always contains Pint-compliant code. Pair it with --test mode in CI so CI catches anything hooks missed.
How do you set up Git hooks with Husky and pre-commit frameworks?
Manual hooks in .git/hooks/ are not version-controlled. They do not travel with the repository. Every clone starts empty. That is the biggest operational gap on team projects.
Two mature solutions fix this: Husky for JavaScript-heavy repos, and the Python pre-commit framework for polyglot PHP/Laravel teams. Both store hook definitions in tracked files and install them on clone.
Option A: Husky with lint-staged
Husky is the standard for Laravel + Vite 8.x frontends where npm 12 manages assets. Official docs live at typicode.github.io/husky. Setup on a fresh clone:
npm install --save-dev husky lint-staged
npx husky init
echo 'npx lint-staged' > .husky/pre-commit Add lint-staged rules to package.json:
{
"lint-staged": {
"*.{js,vue}": ["eslint --fix"],
"*.php": ["vendor/bin/pint"]
}
} Husky stores hooks in .husky/ — a tracked directory. When a teammate clones and runs npm install, the prepare script installs hooks automatically. No manual chmod step.
Option B: pre-commit framework for polyglot repos
The pre-commit framework excels when one repo mixes PHP, JavaScript, YAML, and Docker files. Create .pre-commit-config.yaml:
repos:
- repo: local
hooks:
- id: pint
name: Laravel Pint
entry: vendor/bin/pint
language: system
types: [php]
- id: php-syntax
name: PHP Syntax Check
entry: php -l
language: system
types: [php]
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks Install and activate:
pip install pre-commit
pre-commit install
pre-commit install --hook-type pre-push Run against all files once to baseline existing issues: pre-commit run --all-files. After that, only staged files get checked on each commit.
Option C: core.hooksPath for shell-only teams
If your team avoids Node and Python tooling on the server, point Git at a tracked hooks directory:
git config core.hooksPath .githooks
chmod +x .githooks/* Store scripts in .githooks/pre-commit, .githooks/pre-push, and commit them. Every developer runs one config command after clone. Simple and transparent — ideal for Deployer 7 pipelines where the production server has no Node.js runtime.
On sister sites I deploy with GitLab CI and Deployer 7, this pattern keeps developer laptops aligned with what CI validates. Same scripts, two enforcement points.
How do Git hooks compare to CI pipeline checks?
Hooks and CI are complementary, not competing. Hooks give instant local feedback. CI gives authoritative enforcement on the server where bypass flags do not exist.
| Criteria | Local Git Hooks | CI Pipeline (GitLab CI / GitHub Actions) |
|---|---|---|
| Speed of feedback | Seconds — runs on staged files only | Minutes — full environment spin-up |
| Can be bypassed | Yes — git commit --no-verify | No — merge blocked until green |
| Environment parity | Developer machine — may drift | Standardised runner image |
| Cost | Free — uses local CPU | Runner minutes — Rs 0 locally, cloud CI billed |
| Best for | Format, syntax, secrets, fast unit tests | Integration tests, deploy, security scans |
| Setup complexity | Low — one script or Husky init | Medium — YAML, secrets, cache config |
The winning pattern: fast hooks locally, strict CI remotely. Hooks reduce CI failure rate by 60–80% on teams I've worked with. Fewer red pipelines means faster merges and less context switching.
Read adding AI code review to your CI pipeline for the server-side layer. Hooks handle the easy rejections before AI or human review spends time on obvious problems.
For testing and optimization engagements, I audit both layers. Missing hooks mean developers wait for CI to learn about a missing semicolon. Missing CI means one --no-verify push can break production.
How do you share Git hooks across a team without manual setup?
The operational goal is zero-touch onboarding. A new developer clones, installs dependencies, and hooks work. No wiki page. No Slack reminder to run a script.
Enforce hooks through CI when local bypass happens
Assume hooks will be bypassed occasionally. CI must run the same checks. Duplicate your pre-commit logic in .gitlab-ci.yml:
lint:
stage: test
script:
- vendor/bin/pint --test
- vendor/bin/phpstan analyse
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" Add a CI job that fails if hook installer was never run — check for a marker file or run pre-commit run --all-files on every push. Branch protection rules on GitLab or GitHub block merge until lint passes.
Document the escape hatch responsibly
git commit --no-verify and git push --no-verify exist for emergencies. Hotfix at 11 PM with a broken test suite? Bypass, fix properly in the next commit. Document this in your README — not as encouragement, but as acknowledged reality.
Pair bypass documentation with Git reflog recovery knowledge so developers know how to undo mistakes after a bad push.
Debugging hooks that fail silently
When a hook blocks a commit with no clear output, run it manually:
sh -x .git/hooks/pre-commit The -x flag traces every command. Common failures I see on client projects:
- Wrong PHP binary — laptop uses 8.5, hook calls system
phpat 8.1. Fix with absolute path orasdf/phpenvshims. vendor/bin/pintmissing becausecomposer installwas skipped after merge. Add a post-merge hook.- Line ending differences on Windows clones break shell scripts. Enforce
.gitattributeswith* text=auto. - File permission noise — see stopping Git from tracking file permissions if chmod changes pollute every commit.
Real-world hook stack on a Laravel booking platform
On a Laravel + Livewire booking system similar to Adventure Third Pole Trek, the hook stack looked like this:
- pre-commit: Pint, forbidden path check (
.env,storage/), ESLint onresources/js/ - commit-msg: Conventional Commits regex — required for changelog automation
- pre-push:
php artisan test --parallel— full suite, about 90 seconds - CI: Same checks plus MySQL 9.7 integration tests and Deployer 7 staging deploy
Push failures dropped within the first sprint. Developers stopped treating CI as a linter. The regex tester helped the team iterate on commit-msg patterns without endless trial commits.
Server-side hooks on self-hosted GitLab
Client-side hooks protect the developer. Server-side hooks protect the repository. On self-hosted GitLab, add a pre-receive hook to reject force-pushes to main or commits from unverified authors.
Official Git documentation for all hook types lives at git-scm.com/docs/githooks. Server-side hooks use the same exit-code contract — non-zero blocks the push for every contributor regardless of local configuration.
For teams using Linux server administration on Ubuntu 22/24 GitLab runners, server hooks add a final gate before code reaches a Deployer 7 release path.
Key Takeaways
- Install
pre-commitfor format and secret scans,pre-pushfor tests — keep pre-commit under 10 seconds to prevent--no-verifyabuse. - Version-control hooks via Husky, pre-commit framework, or
core.hooksPath— never rely on manual.git/hooks/copies. - Mirror every local hook check in CI — hooks are fast feedback, CI is authoritative enforcement.
- Block
.env, keys, andstorage/logs/in pre-commit — credential leaks are expensive to unwind. - Debug failing hooks with
sh -x .git/hooks/pre-commitand verify the PHP binary path matches your project requirement. - Place checks by speed: under 5 seconds in pre-commit, under 2 minutes in pre-push, everything else in CI only.
People Also Ask
Can Git hooks be bypassed?
Yes. Running git commit --no-verify or git push --no-verify skips client-side hooks entirely. Server-side hooks and CI branch protection cannot be bypassed from the command line. That is why both layers matter — hooks for speed, CI and branch rules for enforcement.
Do Git hooks work with GitHub Desktop or IDE commits?
Yes. GUI clients and IDE integrations call the same Git CLI under the hood. If hooks are installed in .git/hooks/ or via Husky's core.hooksPath, they run on GUI commits too. If hooks fail silently in an IDE, check whether the IDE uses its own bundled Git binary with a different hooks path.
What is the difference between pre-commit and pre-push hooks?
Pre-commit runs before a commit object is created — best for fast, file-level checks like linting and secret scans. Pre-push runs before refs transfer to the remote — better for slower work like full test suites or build verification. Use both; split checks by execution time.
Should I use Husky or the pre-commit Python framework?
Choose Husky when your repo already uses npm 12 and Node.js 26 LTS for Vite or frontend tooling — it integrates naturally with lint-staged. Choose the pre-commit framework for polyglot repos with PHP, Python, YAML, and Docker files in one project. Both are production-ready in 2026.
Ship cleaner commits with automated hook enforcement
Git Hooks: Automate Checks Before Commit and Push turn code quality from a reminder into a default. Start with one pre-commit script that blocks secrets and runs Pint. Add pre-push tests next. Mirror both in CI. Within a week your pipeline runs cleaner and your team stops fixing formatting in review comments.
If you want help wiring hooks into an existing Laravel, WordPress, or CI workflow, review the Mijar Law Associates client portal and other portfolio projects that ship with enforced quality gates — or contact us to audit your current Git and deployment setup. For related reading, see Git rebase vs merge, fixing Git ignore problems, and automating server setup with Ansible. Explore more on the blog or learn about the author on the about page.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

