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 for Automation

By Kokil Thapa | Last reviewed: September 2026

Git Hooks for Automation let you run scripts at fixed points in the Git lifecycle—before a commit lands, before a push leaves your machine, or after a merge completes. Broken formatting, leaked API keys, and failing unit tests often reach CI because nothing stopped them locally. On production Laravel and WordPress projects I maintain, a five-second hook saves twenty minutes of pipeline time and one awkward revert. This guide covers hook types, real scripts for PHP and JavaScript stacks, team distribution, and where hooks fit beside build pipeline automation best practices.

What Are Git Hooks for Automation and How Do They Work?

Git hooks are programs Git invokes automatically when specific events occur. You do not call them manually. Git looks for an executable file with a standard name—pre-commit, commit-msg, pre-push—and runs it with context about the pending operation.

The contract is simple. The hook receives arguments on stdin or as positional parameters. It prints diagnostics to stderr. If the script exits with code 0, Git continues. Any non-zero exit aborts the operation. That fail-fast behaviour is the core of Git Hooks for Automation.

Hooks live inside .git/hooks/ by default. That directory is not tracked by Git itself, which creates a distribution problem teams solve later in this article. Client-side hooks run on the developer machine. Server-side hooks run on the remote when someone pushes—common on self-hosted GitLab or bare repos, rare on GitHub.com free tiers.

Git Hooks for Automation Lifecyclegit addStage filespre-commitLint and testcommit-msgFormat checkpre-pushFull test suiteRemote CI PipelineGitLab CI or GitHub ActionsExit 1 = blockedExit 0 = proceed
Git Hooks for Automation intercept commits and pushes before code reaches your CI server.

The official Git documentation lists every hook name and trigger point. Read it once when you design your workflow. You will reference it again when debugging why a hook never fires.

Client-side vs server-side hooks

Client-side hooks—pre-commit, prepare-commit-msg, pre-push—run on the developer laptop or workstation. Server-side hooks—pre-receive, update, post-receive—run on the Git remote. For most teams on GitHub or GitLab SaaS, server-side automation moves to branch protection rules and CI jobs instead.

I treat client hooks as fast feedback and CI as the authoritative gate. Neither replaces the other. A developer can bypass local hooks with --no-verify. CI cannot be skipped without admin rights.

Which Git Hook Should You Use for Pre-Commit, Commit-Msg, and Pre-Push?

Pick the hook that matches how expensive the check is. Fast checks belong on pre-commit. Message validation belongs on commit-msg. Slow integration tests belong on pre-push or CI only.

HookWhen it runsTypical automationSpeed target
pre-commitBefore commit object is createdPHPCS, ESLint, Prettier, staged-file lintUnder 10 seconds
commit-msgAfter message is writtenConventional Commits regex, ticket ID formatUnder 1 second
pre-pushBefore refs update on remotePHPUnit subset, npm test, type checkUnder 60 seconds
post-mergeAfter successful mergecomposer install, npm ciVaries
post-checkoutAfter branch switchEnv sync reminders, dependency checkVaries

On a Laravel 12 or 13 project, my default stack looks like this. Pre-commit runs Pint or PHPCS on staged PHP files only. Commit-msg enforces ABC-123: subject line if the team uses Jira. Pre-push runs php artisan test --parallel when the branch is not a draft.

Sample pre-commit hook for PHP and Laravel

Create the file, make it executable, and test before you rely on it.

#!/usr/bin/env bash
set -euo pipefail

STAGED_PHP=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$' || true)

if [ -z "$STAGED_PHP" ]; then
  exit 0
fi

echo "$STAGED_PHP" | xargs -r ./vendor/bin/pint --test

if [ -f package.json ]; then
  echo "$STAGED_PHP" | xargs -r npx prettier --check 2>/dev/null || true
fi

exit 0

Save that as .git/hooks/pre-commit and run chmod +x .git/hooks/pre-commit. Pint ships with Laravel; adjust the binary path if you use PHPCS instead. The script exits early when no PHP files are staged, which keeps commits to markdown or config files snappy.

Sample commit-msg hook with regex

Validate the first line before the commit is final. A regex tester helps you tune the pattern without ten failed commits.

#!/usr/bin/env bash
COMMIT_MSG_FILE=$1
FIRST_LINE=$(head -n1 "$COMMIT_MSG_FILE")

PATTERN='^(feat|fix|docs|chore|refactor)(\([a-z0-9_-]+\))?: .{10,72}$'

if ! echo "$FIRST_LINE" | grep -Eq "$PATTERN"; then
  echo "Commit message must match Conventional Commits:" >&2
  echo "  feat(scope): description at least 10 chars" >&2
  exit 1
fi

Conventional Commits improve changelog generation and pair well with semantic release tooling. Keep the regex readable. Overly strict patterns frustrate the team within a week.

Sample pre-push hook for tests

Pre-push receives remote name and URL. Use it for checks that need bootstrapped frameworks.

#!/usr/bin/env bash
set -euo pipefail

while read local_ref local_sha remote_ref remote_sha; do
  if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
    continue
  fi
  echo "Running tests before push to $remote_ref..."
  php artisan test --parallel
done

exit 0

If tests take more than a minute, scope them. Run the full suite on pre-push to main only. Run a smoke subset on feature branches. Document the rule in your README so new hires know what to expect.

How Do You Set Up Git Hooks for Automation in a Laravel or PHP Project?

Manual hooks in .git/hooks/ work for solo work. Teams need version-controlled hook scripts plus a one-time Git config step. Two patterns dominate in 2026: core.hooksPath and the Husky npm package.

For PHP-heavy repos without a Node toolchain on the server, core.hooksPath is my first choice. For Laravel apps that already use Vite 8.x and npm 12 for assets, Husky integrates cleanly with npm scripts for build automation.

Shared Git Hooks for AutomationRepo: .githooks/Tracked in Gitgit configcore.hooksPathDeveloper cloneHooks activecomposer.jsonpost-install scriptMakefile targetmake hooks-installEvery commit runs same checksNo manual copy to .git/hooks
Version-controlled hook scripts plus core.hooksPath distribute Git Hooks for Automation across every clone.

Method 1: core.hooksPath (framework-agnostic)

  1. Create a directory at the repo root, commonly .githooks/ or scripts/git-hooks/.
  2. Move your hook scripts there. Keep the exact hook names Git expects.
  3. Run chmod +x .githooks/* so Git can execute them.
  4. Point Git at the directory: git config core.hooksPath .githooks.
  5. Add a Composer post-install script or documented Makefile target so new clones set the path automatically.
{
  "scripts": {
    "post-install-cmd": [
      "git config core.hooksPath .githooks || true"
    ]
  }
}

The || true prevents Composer from failing in CI containers that use a tarball export without a .git directory. That detail has saved more than one Deployer 7 pipeline I've maintained on shared EC2 hosts.

Method 2: Husky with npm 12

When the repo already ships a package.json, Husky 9.x manages hook wiring through npm lifecycle scripts.

npm install --save-dev husky
npx husky init
echo "npm run lint-staged" > .husky/pre-commit

Pair Husky with lint-staged so only changed files get checked. Without lint-staged, pre-commit runs against the entire codebase and developers start using --no-verify daily.

Method 3: pre-commit framework (polyglot repos)

The Python pre-commit tool works well when one repo mixes PHP, JavaScript, Terraform, and Markdown. You define hooks in .pre-commit-config.yaml. Install once with pre-commit install. It supports PHPCS, Pint wrappers, ESLint, and secret scanners out of the box.

On legal-tech portals and booking systems I've shipped with Laravel and Livewire, pre-commit framework reduced "works on my machine" lint drift between Kathmandu and remote contractors. Everyone runs the same hook versions pinned in YAML.

How Do Git Hooks Compare to CI/CD Pipelines for Automation?

Hooks and CI solve different layers of the same problem. Hooks give instant feedback on the machine where edits happen. CI runs in a clean environment with secrets, databases, and deployment targets hooks cannot access.

CriteriaGit Hooks for AutomationCI/CD (GitLab CI, GitHub Actions)
Runs whereDeveloper workstationRemote runner or container
EnvironmentMay differ from productionReproducible image per job
Can be skippedYes (git commit --no-verify)Only with bypass permissions
Typical durationSeconds to one minuteMinutes to tens of minutes
Best forFormat, lint, quick unit testsIntegration tests, builds, deploys
Secrets scanningBasic pattern match locallyFull gitleaks job with SARIF upload

Neither row wins outright. The winning pattern is shift-left checks to hooks and keep authoritative enforcement in CI with required status checks on protected branches. Read how git hooks automate checks before commit and push alongside secrets scanning in Git and CI with gitleaks for a layered security model.

Hooks vs CI: Two LayersLocal HooksFast feedbackLint and formatStaged files onlySkippableShift-left qualityRemote CIClean containerFull test suiteBuild artifactsBranch protectionAuthoritative gatepushUse both — hooks reduce CI failures
Git Hooks for Automation complement CI; they do not replace branch protection and pipeline gates.

GitLab CI and similar systems can mirror hook commands in the before_script or a dedicated lint stage. Duplication is acceptable when the commands are identical one-liners. Extract shared logic into shell scripts under scripts/ci/ and call them from both Husky and .gitlab-ci.yml.

What Security and Quality Checks Belong in Git Hooks for Automation?

Prioritise checks that fail often and cheaply. Deprioritise anything that needs Docker, cloud credentials, or a full MySQL 9.7 import.

  • Formatting: Laravel Pint, PHP-CS-Fixer, Prettier, or Black depending on stack.
  • Static analysis: PHPStan level 5+ on changed paths, ESLint with flat config for JS.
  • Secret detection: grep for AKIA, private key headers, or gitleaks protect --staged.
  • Dependency sanity: block commits that modify composer.lock without composer.json.
  • File hygiene: reject commits adding .env, vendor/, or node_modules/.
  • Commit metadata: enforce ticket references for audit trails on regulated client work.

On document-heavy legal-tech portals, I also block commits that add uncompressed PDFs over 5 MB to Git. Those belong in object storage, not history. A pre-commit size check saves repo bloat that Git LFS for large files would otherwise patch late.

Automating Composer and npm consistency

Post-merge hooks keep dependencies aligned after pulling main. A minimal post-merge script:

#!/usr/bin/env bash
changed=$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)

echo "$changed" | grep -q '^composer.lock$' && composer install --no-interaction
echo "$changed" | grep -q '^package-lock.json$' && npm ci

Developers forget composer install after merges more often than they admit. The hook removes one support ticket category entirely.

Hook performance on modest hardware

Not every developer in Nepal runs a MacBook Pro. Budget laptops struggle when pre-commit runs PHPStan on 400 files. Scope tools to staged paths. Cache PHPStan result caches under tmp/. Skip frontend lint when only backend files changed.

If a hook exceeds ten seconds regularly, split it. Move heavy work to pre-push or a CI lint stage. Slow hooks train teams to bypass them, which defeats the purpose.

What Are Common Git Hooks Automation Mistakes Teams Should Avoid?

Most hook failures I debug are operational, not syntax errors. The script runs fine when executed manually but never triggers from Git.

Git Hooks for Automation PitfallsMissing chmod +xHook silently skippedHooks too slowTeam uses --no-verifyNot in repo.git/hooks untrackedWindows line endingsCRLF breaks shebangFix: core.hooksPath + lint-stagedDocument setup in README and Composer scriptsTest with: git commit --allow-empty -m "test: hook"
Avoid these Git Hooks for Automation pitfalls that cause teams to disable local quality gates.

Mistake 1: Hooks only in .git/hooks

Files inside .git/ never get pushed. New clones start with sample .sample files only. Always store real scripts in the tracked tree and install them via config or a setup script.

Mistake 2: No CI backstop

Hooks are voluntary on the client. A contractor, intern, or tired senior will bypass them. Mirror every critical hook step in CI and require the job on merge requests. Testing and optimization services often start with auditing this gap.

Mistake 3: Hard-coded absolute paths

/home/kokil/.phpenv/shims/php breaks on every other machine. Use #!/usr/bin/env bash, command -v php, or Composer bin stubs. Portable hooks survive team growth.

Mistake 4: Blocking urgent hotfixes

Production-down hotfixes need a path. Document when --no-verify is acceptable and require a follow-up commit that fixes lint debt. Branch protection on main still catches issues before deploy when CI runs.

Mistake 5: Ignoring GUI clients

GitKraken, Sourcetree, and IDE commit dialogs still invoke hooks if configured correctly. They do not always surface stderr clearly. Test hooks from both terminal and GUI so developers are not surprised.

For sister sites I deploy with Deployer 7 and GitLab CI—legal portals, translation services, and similar—a broken hook on one developer laptop once blocked an entire release window. We moved hook scripts into the repo, added a make setup target, and documented the flow in the same README that covers rollback. Problem rate dropped immediately.

Remote automation still has a place. Laravel Envoy for remote task automation handles server-side tasks hooks cannot touch. Managing dotfiles and server config with Git keeps personal Git config separate from project hook paths.

Key Takeaways

  • Git Hooks for Automation run locally at events like pre-commit and pre-push; exit code 1 blocks the Git operation.
  • Store hook scripts in a tracked directory and set git config core.hooksPath so every clone gets the same checks.
  • Keep pre-commit fast with staged-file lint; reserve heavy PHPUnit or integration runs for pre-push or CI.
  • Mirror critical hook steps in GitLab CI with required jobs on protected branches—hooks alone are not enforcement.
  • Use lint-staged or pre-commit framework to avoid scanning the entire repo on every commit.
  • Test hooks with an empty commit after setup; verify execute permissions and shebang line endings on all OS targets.

People Also Ask

Can Git hooks be version controlled?

Not when placed directly in .git/hooks/, because Git ignores its own metadata directory. The standard fix is a tracked folder like .githooks/ combined with core.hooksPath or a tool like Husky that installs from package.json. Commit the scripts, not the internal .git path.

How do you skip a Git hook temporarily?

Pass --no-verify (or -n) to git commit or git push. Use sparingly for genuine emergencies. Teams that normalise skipping hooks lose the automation benefit within weeks. CI branch protection should still catch problems before merge.

Do Git hooks work with GitHub Desktop and IDE commits?

Yes, when hooks are installed correctly on the local repository. GUI tools invoke the same Git binary and hook path. If checks never run, confirm core.hooksPath, file permissions, and that the GUI uses the system Git rather than an embedded minimal build.

Are server-side Git hooks available on GitHub?

GitHub.com does not expose pre-receive hooks on standard plans. GitHub Enterprise Server and self-hosted GitLab do. Most teams replace server-side hooks with CI status checks and merge request rules, which are easier to audit and log.

Build Reliable Git Hooks for Automation on Your Next Project

Git Hooks for Automation cost an afternoon to set up and pay back on the first prevented bad commit. Start with pre-commit Pint or ESLint on staged files, add commit-msg format rules, and mirror the same commands in CI. Keep scripts in the repo, automate installation through Composer or npm, and document bypass policy for hotfixes.

If you want hooks wired into a Laravel 13 app, a WooCommerce 11.1 shop, or a GitLab CI pipeline on Ubuntu, I can help design the local and remote layers together. See the Adventure Third Pole Trek booking platform and other portfolio projects for production workflows that combine Git discipline with deploy automation.

For full-service setup—including hook scripts, CI lint stages, and server hardening—explore custom software development in Nepal or support and maintenance services. Need a JSON config validator while building hook output parsers? Try the JSON formatter tool. Ready to talk through your repo? Contact us with your stack and current pain points.

Frequently Asked Questions

Git hooks for automation are executable scripts Git runs at fixed lifecycle events—pre-commit, pre-push, post-merge—without you calling them manually. Exit code 0 allows the operation; any non-zero exit aborts it, giving local lint, test, and security gates before code reaches remote CI.

Not when placed directly in .git/hooks/, because Git ignores its own metadata directory. Store scripts in a tracked folder like .githooks/ or scripts/git-hooks/, then point every clone at it with git config core.hooksPath .githooks so the team shares identical hook logic.

Pre-commit runs before the commit object is created—ideal for fast staged-file lint under ten seconds. Pre-push runs before refs update on the remote—better for slower PHPUnit or npm test runs up to about sixty seconds. Pick the hook that matches how expensive the check is.

For PHP-heavy repos, create .githooks/ with executable pre-commit, commit-msg, and pre-push scripts, then run git config core.hooksPath .githooks. Add a Composer post-install-cmd entry so new clones set the path automatically. Laravel apps already using Vite 8.x and npm 12 can alternatively wire hooks through Husky 9.x and lint-staged.

Yes. Client-side hooks are voluntary; git commit --no-verify and git push --no-verify skip them. Treat hooks as fast feedback, not enforcement. Mirror every critical hook step in GitLab CI or GitHub Actions with required status checks on protected branches so bypassed commits still fail before merge.

Hooks run on the developer workstation in seconds, catching format and quick unit test failures instantly. CI runs in a reproducible remote container with databases, secrets, and deployment targets hooks cannot access. Neither replaces the other; shift-left checks to hooks and keep authoritative enforcement in CI with branch protection.

Keep pre-commit fast—under ten seconds. Run Laravel Pint or PHPCS on staged PHP files only, ESLint or Prettier on changed JavaScript, and basic secret pattern scans on staged content. Skip full PHPUnit suites, Docker builds, or anything needing a MySQL 9.7 import; those belong on pre-push or in CI.

core.hooksPath tells Git to look for hook scripts in a version-controlled directory instead of .git/hooks/. After creating .githooks/ with correctly named executable files, run git config core.hooksPath .githooks once per clone. Pair it with a Composer post-install script using || true so CI tarball exports without a .git directory do not fail the install step.

For PHP-heavy repos without a Node toolchain on the server, core.hooksPath is the simpler choice—no npm dependency required. For Laravel apps already shipping package.json with Vite 8.x and npm 12, Husky 9.x integrates cleanly via npm lifecycle scripts and pairs well with lint-staged for staged-file checks only.

The most common causes are operational, not syntax errors. Verify the script is executable, lives at the path Git expects, and has a proper shebang like #!/usr/bin/env bash. If hooks sit only in .git/hooks/, new clones start with sample files only. Confirm core.hooksPath is set, test with an empty commit, and check shebang line endings on Windows clones.

Target under ten seconds. Scope tools to staged paths only—run Pint on changed PHP files, not the entire codebase. If a hook regularly exceeds ten seconds, split it: keep fast lint on pre-commit and move PHPStan, full PHPUnit, or integration tests to pre-push or a CI lint stage. Slow hooks train teams to use --no-verify daily.

Prioritise cheap, high-value checks: grep staged files for AKIA patterns or private key headers, run gitleaks protect --staged, block commits adding .env, vendor/, or node_modules/, and reject composer.lock changes without a matching composer.json edit. Full SARIF-upload secret scans with gitleaks belong in CI; hooks catch obvious leaks before push.

The Python pre-commit tool manages hooks via .pre-commit-config.yaml and installs them with pre-commit install. It works well in polyglot repos mixing PHP, JavaScript, Terraform, and Markdown because everyone runs the same hook versions pinned in YAML. On Laravel and Livewire projects with remote contractors, it reduced lint drift between machines.

Yes, GUI clients and IDE commit dialogs invoke hooks when configured correctly—they do not bypass them automatically. The catch is stderr output is not always surfaced clearly in the UI, so a failing hook can look like a silent error. Test hooks from both terminal and GUI after setup so developers are not surprised mid-commit.

Five recurring pitfalls: hooks stored only in .git/hooks/ so new clones get nothing; no CI backstop because hooks can be skipped; hard-coded absolute paths like /home/user/.phpenv/shims/php; blocking urgent hotfixes with no documented --no-verify policy; and never testing from GUI clients. Store scripts in the repo, add a make setup target, and mirror critical steps in GitLab CI.

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: