Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Git Hooks: Automate Checks Before Commit and Push

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.

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.

Git Hooks LifecycleWorking Treeedited filespre-commitlint + formatcommit-msgmessage rulespre-pushtests + buildRemote RepoGitLab / GitHubExit code 0 = proceed | Exit code 1+ = block actionHooks run locally before CI — fast feedback at commit time
Git hooks automate checks before commit and push at three critical lifecycle points on the developer machine

Client-side hooks that matter most

These hooks appear in almost every production workflow I set up:

  • pre-commit — runs after git commit is 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 install when composer.lock changed.

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.

  1. pre-commit: Laravel Pint (vendor/bin/pint --test), PHPStan or Larastan on changed files, block commits containing .env, storage/logs/, or private keys.
  2. commit-msg: Require format like feat(booking): add deposit payment flow — matches patterns from trunk-based and GitFlow branching.
  3. pre-push: Run php artisan test or Pest suite, verify composer validate, optionally run frontend build with Vite 8.x if assets changed.
  4. post-merge: Auto-run composer install --no-interaction when 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.

Shared Team Hook Setupgit clonefresh repoTracked Config.husky/ or .pre-commitInstall Stepnpm / pip installActivehooks runHusky (Node.js 26 LTS).husky/pre-commitlint-staged + npm scriptspre-commit (Python).pre-commit-config.yamlPHP, JS, secrets, YAMLcore.hooksPath or framework installer writes to .git/hooks/Same Git hook mechanism — version-controlled source of truth
Shared Git hooks travel with the repository via Husky or pre-commit framework installers

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.

CriteriaLocal Git HooksCI Pipeline (GitLab CI / GitHub Actions)
Speed of feedbackSeconds — runs on staged files onlyMinutes — full environment spin-up
Can be bypassedYes — git commit --no-verifyNo — merge blocked until green
Environment parityDeveloper machine — may driftStandardised runner image
CostFree — uses local CPURunner minutes — Rs 0 locally, cloud CI billed
Best forFormat, syntax, secrets, fast unit testsIntegration tests, deploy, security scans
Setup complexityLow — one script or Husky initMedium — 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.

Hooks vs CI — Defence in DepthLocal Git Hookspre-commit: Pint, ESLintpre-push: PHPUnit subsetFeedback: 2–15 secondsBypassable with --no-verifyCI PipelineFull test suite + buildSecret scan + deploy previewFeedback: 3–12 minutesRequired for mergeMerge to main branchHooks catch 80% of issues earlyCI catches the rest — no bad code shipsDeployer 7 deploy only after CI green
Git hooks and CI pipelines form two layers — local speed plus server-side enforcement before deploy

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 php at 8.1. Fix with absolute path or asdf/phpenv shims.
  • vendor/bin/pint missing because composer install was skipped after merge. Add a post-merge hook.
  • Line ending differences on Windows clones break shell scripts. Enforce .gitattributes with * text=auto.
  • File permission noise — see stopping Git from tracking file permissions if chmod changes pollute every commit.
Where Should This Check Run?New automated checkRuns in under 5 seconds?YESpre-commitNOUnder 2 min?pre-push candidateYESpre-push hookNOCI onlySecret scans and syntax checks always run in pre-commit regardless of speed
Decision tree for placing checks in pre-commit, pre-push, or CI based on execution time

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 on resources/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-commit for format and secret scans, pre-push for tests — keep pre-commit under 10 seconds to prevent --no-verify abuse.
  • 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, and storage/logs/ in pre-commit — credential leaks are expensive to unwind.
  • Debug failing hooks with sh -x .git/hooks/pre-commit and 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

Git hooks are executable scripts Git runs at lifecycle events like commit, push, or merge. They automate checks by running shell commands on your machine — lint, format, secret scans before commit; tests before push — and block the action if a script exits non-zero.

Git stores hooks in .git/hooks/ and invokes them when you run git commit or git push. Each hook receives context through environment variables and stdin. Exit code 0 allows the action; any non-zero code blocks it. Copy a .sample file, remove the extension, chmod +x, and Git picks it up immediately with no restart required.

Pre-commit runs before the commit object is created — best for fast file-level checks like Pint, ESLint, and secret scans. Pre-push runs before refs update on the remote — suited for slower work like php artisan test or composer validate.

Yes. git commit --no-verify and git push --no-verify skip client-side hooks entirely. Server-side hooks and CI branch protection cannot be bypassed from the command line.

On production Laravel applications I maintain, pre-commit runs Pint, PHPStan or Larastan on changed files, and blocks .env, storage/logs/, or private keys. commit-msg enforces Conventional Commits like feat(booking): add deposit payment flow. pre-push runs php artisan test and composer validate. post-merge auto-runs composer install when composer.lock changes. For WordPress 7.1 or WooCommerce 11.1 theme projects, swap Pint for PHPCS with WordPress coding standards — the hook structure stays identical.

Create .git/hooks/pre-commit as a shell script that lists staged PHP files with git diff --cached --name-only --diff-filter=ACM, then runs php -l on each. If syntax fails, echo an error and exit 1. Make it executable with chmod +x .git/hooks/pre-commit. Stage a file with a syntax error and try committing — Git blocks it. Fix the file, stage again, and the hook passes. This works on any Laravel 12 or Symfony project without extra dependencies.

Add a pre-commit grep check against staged filenames matching patterns like .env, id_rsa, .pem, or credentials.json. If matched, echo a block message and exit 1. I've seen .env fragments committed because a developer force-added a config override — a five-line grep check prevents hours of credential rotation. For deeper scanning, integrate Gitleaks in pre-commit on staged diffs and again in GitLab CI for defence in depth.

Laravel Pint ships with Laravel 12 and 13. A practical wrapper lists staged PHP files, runs vendor/bin/pint via xargs on them, then git add those files back so the commit contains Pint-compliant code. Pair auto-fix in hooks with --test mode in CI so CI catches anything hooks missed. A common failure is vendor/bin/pint missing because composer install was skipped after merge — add a post-merge hook to fix that.

Manual hooks in .git/hooks/ are not version-controlled and do not travel with the repository. Husky suits Laravel plus Vite 8.x frontends: npm install husky lint-staged, npx husky init, point .husky/pre-commit at npx lint-staged, and define rules in package.json for ESLint and Pint. The pre-commit framework excels in polyglot repos — define hooks in .pre-commit-config.yaml, pip install pre-commit, then pre-commit install and pre-commit install --hook-type pre-push. Both store definitions in tracked files and install on clone.

core.hooksPath points Git at a tracked hooks directory instead of .git/hooks/. Run git config core.hooksPath .githooks, store scripts like .githooks/pre-commit and .githooks/pre-push, chmod +x them, and commit the directory. Every developer runs one config command after clone. This suits shell-only teams that avoid Node and Python tooling on the server — ideal for Deployer 7 pipelines where production has no Node.js runtime.

Hooks and CI are complementary, not competing. Hooks give instant feedback in seconds on staged files only; CI takes minutes on a standardised runner. Hooks can be bypassed with --no-verify; CI branch protection cannot. Hooks are free local CPU; cloud CI bills runner minutes. The winning pattern: fast hooks locally, strict CI remotely. Hooks reduce CI failure rate by 60–80% on teams I've worked with. Missing hooks means developers wait for CI to learn about a missing semicolon; missing CI means one --no-verify push can break production.

The operational goal is zero-touch onboarding — clone, install dependencies, hooks work with no wiki page. Version-control hooks via Husky, the pre-commit framework, or core.hooksPath so they travel with the repository. Assume hooks will be bypassed occasionally: duplicate pre-commit logic in .gitlab-ci.yml with jobs like vendor/bin/pint --test and phpstan analyse on merge requests. Branch protection on GitLab or GitHub blocks merge until lint passes. Document --no-verify as an emergency escape hatch in your README, not as encouragement.

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 — PHP syntax, Pint, ESLint on staged files, secret scanning. Push heavier work to pre-push or CI. Place checks by speed: under 5 seconds in pre-commit, under 2 minutes in pre-push, everything else in CI only. On a Laravel booking platform I set up, pre-push ran php artisan test --parallel for about 90 seconds while pre-commit stayed instant.

Run the hook manually with sh -x .git/hooks/pre-commit — the -x flag traces every command. Common failures I see on client projects: wrong PHP binary where the laptop uses 8.5 but the hook calls system php at 8.1 — fix with absolute path or asdf/phpenv shims; vendor/bin/pint missing after merge; line ending differences on Windows clones breaking shell scripts — enforce .gitattributes with text=auto; file permission noise from chmod changes polluting every commit.

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. Server-side pre-receive hooks on self-hosted GitLab add a final gate — they reject force-pushes to main or commits from unverified authors regardless of local configuration, using the same exit-code contract as client hooks.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: