
September 11, 2026
14 min read
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.
.git/hooks/ (or a shared path via core.hooksPath) that Git runs at events like pre-commit and pre-push. Exit code 1 blocks the action; exit code 0 allows it—giving you local lint, test, and security gates before code reaches remote CI.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.
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.
| Hook | When it runs | Typical automation | Speed target |
|---|---|---|---|
pre-commit | Before commit object is created | PHPCS, ESLint, Prettier, staged-file lint | Under 10 seconds |
commit-msg | After message is written | Conventional Commits regex, ticket ID format | Under 1 second |
pre-push | Before refs update on remote | PHPUnit subset, npm test, type check | Under 60 seconds |
post-merge | After successful merge | composer install, npm ci | Varies |
post-checkout | After branch switch | Env sync reminders, dependency check | Varies |
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.
Method 1: core.hooksPath (framework-agnostic)
- Create a directory at the repo root, commonly
.githooks/orscripts/git-hooks/. - Move your hook scripts there. Keep the exact hook names Git expects.
- Run
chmod +x .githooks/*so Git can execute them. - Point Git at the directory:
git config core.hooksPath .githooks. - 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.
| Criteria | Git Hooks for Automation | CI/CD (GitLab CI, GitHub Actions) |
|---|---|---|
| Runs where | Developer workstation | Remote runner or container |
| Environment | May differ from production | Reproducible image per job |
| Can be skipped | Yes (git commit --no-verify) | Only with bypass permissions |
| Typical duration | Seconds to one minute | Minutes to tens of minutes |
| Best for | Format, lint, quick unit tests | Integration tests, builds, deploys |
| Secrets scanning | Basic pattern match locally | Full 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.
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, orgitleaks protect --staged. - Dependency sanity: block commits that modify
composer.lockwithoutcomposer.json. - File hygiene: reject commits adding
.env,vendor/, ornode_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.
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.hooksPathso 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
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.

