I've lost count of how many PRs I've reviewed where the only changes in half the files were code style fixes. Someone pushed without running Pint, CI caught it, they fixed it, and now the diff is full of noise. It's a solved problem — you just need to automate it at the right point.
Laravel Pint is a zero-configuration code style fixer built on top of PHP-CS-Fixer. Every Laravel project ships with it. What most teams don't do is enforce it before the commit lands — they leave it for CI to catch instead. Here's how to fix that with a Git pre-commit hook.
Why automate code style with Pint?#
Pint replaces PHP-CS-Fixer with sensible defaults baked in. Run ./vendor/bin/pint and it reformats your PHP files to match Laravel's opinionated style — trailing commas, import ordering, spacing, all of it.
The problem with relying on CI is feedback latency. You push, wait for a pipeline, see a style failure, fix it, push again. A pre-commit hook cuts that loop to zero: the formatting happens on your machine, on only the files you're about to commit, before the commit is even created.
Creating a pre-commit hook manually#
Git hooks live in .git/hooks/. Create a pre-commit file there and make it executable:
touch .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
Then add this script:
#!/bin/bash
# Get staged PHP files only
STAGED_PHP_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep "\.php$")
if [ -z "$STAGED_PHP_FILES" ]; then
exit 0
fi
echo "Running Laravel Pint on staged files..."
# Run Pint on staged PHP files, then re-stage the formatted versions
./vendor/bin/pint $STAGED_PHP_FILES
# Re-add the files Pint may have reformatted
git add $STAGED_PHP_FILES
The --diff-filter=ACMR flag limits to Added, Copied, Modified, and Renamed files — it skips deletions. The --cached flag means we only look at what's staged, not everything in the working tree. After Pint runs, git add re-stages any files it reformatted so the commit includes the fixed versions.
One edge case: if you have a large monorepo with many PHP files touched in a single commit, this can be slow. In that case, use ./vendor/bin/pint --parallel $STAGED_PHP_FILES to spread the work across CPU cores.
Sharing hooks across the team with a Makefile#
.git/hooks/ isn't committed to the repo, which means every developer has to set this up manually. A Makefile target solves that:
.PHONY: install-hooks
install-hooks:
@echo "Installing Git hooks..."
@cp scripts/pre-commit .git/hooks/pre-commit
@chmod +x .git/hooks/pre-commit
@echo "Done."
Move the pre-commit script to scripts/pre-commit in your repo root (committed alongside your code), then document in your README that new team members should run:
make install-hooks
I typically add a note to the project README and also mention it in onboarding. You can also trigger this automatically via Composer's post-autoload-dump hook in composer.json:
{
"scripts": {
"post-autoload-dump": [
"@php artisan package:discover --ansi",
"[ -d .git ] && cp scripts/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit || true"
]
}
}
The [ -d .git ] guard prevents it from failing in CI environments where there's no .git directory.
Alternative: lint-staged with Husky#
If your project already has a package.json (common with Vite and Inertia setups), lint-staged with Husky is a cleaner cross-platform option. It handles the staged-files filtering for you:
npm install --save-dev husky lint-staged
npx husky init
Then update .husky/pre-commit:
npx lint-staged
And add the lint-staged config to package.json:
{
"lint-staged": {
"**/*.php": "./vendor/bin/pint"
}
}
The trade-off: this adds Node.js tooling as a dependency for a PHP workflow. I use the pure bash approach on PHP-only projects and the Husky approach on anything with a frontend build step already.
Either way, the result is the same — code style gets fixed before it ever leaves your machine, CI stays green, and your PRs contain only meaningful changes.
Code style is one layer of automated quality enforcement. For structural rules — keeping controllers thin, banning debug calls, enforcing that all jobs implement ShouldQueue — enforcing Laravel architecture rules with Pest's arch() helper is the natural complement that runs at test time. For security, adding a composer audit step alongside Pint in your pre-commit hook catches known CVEs before they reach CI; auditing PHP dependencies covers the full audit workflow. Both patterns sit inside The Complete Laravel Developer Toolchain for 2026.
FAQ#
Why run Pint in a pre-commit hook instead of relying on CI?
Pre-commit hooks catch style issues on your machine immediately, before the commit exists. CI failures add latency: you push, wait for pipelines, see the failure, fix it, and push again. A hook cuts that loop to zero — you run git commit, Pint reformats silently, and the commit includes the corrected code. Developers see instant feedback and CI stays green.
What happens if Pint is slow on a large monorepo with many PHP files?
Add the --parallel flag: ./vendor/bin/pint --parallel $STAGED_PHP_FILES. This spreads the work across CPU cores and typically reduces runtime by 50–70%. Test on your machine first to ensure you don't have CPU bottlenecks on the CI runner.
Can I skip the hook temporarily if I'm in a hurry?
Yes: git commit --no-verify bypasses the hook. However, this defeats the purpose — your commit includes unformatted code and CI will fail. If you find yourself skipping often, the hook is probably too slow; switch to the Husky approach or add --parallel.
Should I also run composer audit in the same hook?
Yes. Add a line before Pint: ./vendor/bin/composer audit || true to catch known CVEs without blocking the commit (the || true makes it non-fatal, or || exit 1 to fail hard). Auditing dependencies and formatting code in one pre-commit hook enforces quality at the source.