Laravel 13 Git Hooks with Husky: Pint on Every Commit

In this tutorial, I will show you step by step how to set up git hooks in Laravel 12 with Husky, with a complete example from a production Laravel + React (Inertia) repo.
The goal: nobody can commit unformatted PHP, and nobody can push broken JavaScript — while keeping every hook fast enough that no one is ever tempted to bypass it.
Step 1 — Install Husky
npm install --save-dev husky
npx husky init
This creates a .husky/ directory and adds a prepare script to package.json, so every developer gets the hooks automatically on npm install:
"scripts": {
"prepare": "husky"
}
Step 2 — The pre-commit hook: Pint on staged files only
The naive version runs Pint on the whole project. Don't — it reformats files your teammate is mid-way through, and it's slow. The correct version formats only the PHP files in this commit, then re-stages them so the fixes ride along:
# .husky/pre-commit
STAGED_PHP=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$' || true)
if [ -n "$STAGED_PHP" ]; then
echo "husky: pint on staged PHP files…"
./vendor/bin/pint $STAGED_PHP
git add $STAGED_PHP
fi
Three details that matter:
--diff-filter=ACM— added, copied, modified. Deleted files are excluded, otherwise Pint errors on paths that no longer exist.|| true— grep exits non-zero when nothing matches, which would abort every PHP-free commit.git add $STAGED_PHP— Pint's fixes are re-staged, so what lands in the commit is the formatted version. No follow-up "apply pint" commits, ever.
Step 3 — The pre-push hook: the fast test wall
# .husky/pre-push
echo "husky: JS tests…"
npm run test:js
echo "husky: pint check…"
./vendor/bin/pint --test
# Optional: uncomment to also run a critical PHP test group (keep it FAST)
# composer test:critical
On our repo the JS suite runs in about 3 seconds and pint --test (check-only mode, no writes) in about 2. A failed push looks like this and never leaves the machine:
husky: JS tests…
FAIL resources/js/__tests__/invoice-totals.test.ts
error: failed to push some refs
Why the full PHP suite is deliberately NOT in the hooks
Our full Laravel suite is 340+ tests against a real MySQL server — several minutes. Here is the uncomfortable truth from every team I've taught this to: any hook slower than about ten seconds gets bypassed. Developers discover git push --no-verify on day two, and now your "safety net" is theatre.
So the contract is: hooks catch the cheap mistakes instantly, and the full suite runs in Jenkins on every push — where it blocks the deploy, not the developer. That pipeline is exactly what the rest of this series builds.
Verify it works
Deliberately mangle a file and try to commit:
echo 'class Foo{public function bar( ){return 1 ;}}' >> app/Models/Scratch.php
git add app/Models/Scratch.php && git commit -m "test hook"
# → pint reformats it, re-stages, commit lands clean
git show --stat HEAD
Next in the series: post #3 installs Jenkins on Ubuntu 24.04 — including the two apt traps that produce the infamous "Package 'jenkins' has no installation candidate" error in 2026.
Frequently Asked Questions
Why not run PHPUnit in the pre-push hook?
Does Husky work for PHP projects?
Senior Full Stack Developer · Building SaaS products & teaching Laravel/React · 10+ years experience · Founder of Orion360 · Based in Dubai, UAE.
Was this post helpful?



