laravel/pint
Laravel Pint is an opinionated PHP code style fixer for minimalists. Built on PHP-CS-Fixer, it makes it simple to ensure your code style stays clean and consistent, with zero configuration required and an easy-to-use CLI that works in any Laravel or PHP project.
Install and first run (5 minutes)
composer require laravel/pint --dev
./vendor/bin/pint --test # survey only: reports violations, changes nothing
--test is the safest first command on an existing codebase — it tells you how large the diff will be before you touch a single file. If the list is huge, do the first full run as an isolated commit (./vendor/bin/pint && git commit -m "chore: apply Pint") and add that SHA to .git-blame-ignore-revs so git blame stays useful.
Where to look first
./vendor/bin/pint --help — the authoritative list of flags for your pinned version. Pint ships frequently; flag behavior has changed across minor releases.laravel.com/docs/pint) — presets, config keys, CI recipes.pint.json at your project root — the single file you will actually maintain.First real use case: enforce on changed files only
./vendor/bin/pint --dirty # fixes only files with uncommitted git changes
Wire this into your pre-commit / local loop. It is fast, produces small reviewable diffs, and avoids reformatting files nobody touched. Commit --dirty to your muscle memory before you ever run a whole-tree format.
Minimal pint.json
{
"preset": "laravel"
}
That is genuinely all most Laravel apps need. Everything below is deviation management.
Daily local loop
./vendor/bin/pint --dirty # before each commit
./vendor/bin/pint --dirty --diff # see what it wants to change first
./vendor/bin/pint --dirty --test # assert clean, non-mutating
--diff pairs well with --test in code review prep: you get a report of violations plus the exact patch Pint would apply, without modifying the working tree.
CI gate
- run: ./vendor/bin/pint --test
--test exits non-zero when violations exist — that is your gate. Note the inverse gotcha: a plain ./vendor/bin/pint (fix mode) exits 0 even when it rewrote files, so it silently passes CI while changing the checkout. If you want "fail if anything was fixed", use --with-exit-status or just use --test. In GitHub Actions, run --test --diff so the failing job prints the patch in the logs.
Composer script (project-wide ergonomics)
"scripts": {
"lint": "pint --test",
"fix": "pint"
}
Then composer fix / composer lint. This abstracts away vendor path differences and works identically in Sail, Docker, and local.
Adopting on a legacy codebase (incremental strategy)
--test repo-wide, capture the count.--dirty enforced in CI on PRs, and format whole-tree directory by directory (./vendor/bin/pint app/Models, app/Http, …) in separate commits.exclude in pint.json and shrink the list over time.Passing a path argument (./vendor/bin/pint app/Services) is the cheapest way to carve out a formatting beachhead.
Customizing without forking the preset
{
"preset": "laravel",
"exclude": ["app/Legacy", "database/migrations/2020_*"],
"rules": {
"declare_strict_types": true,
"final_class": false,
"Pint/phpdoc_type_annotations_only": true
}
}
Two important mechanics:
preset sets the baseline; rules is a per-rule override layer on top. You are not redefining the preset, only patching it.Pint/... are Laravel's own custom fixers layered over PHP-CS-Fixer (e.g. Pint/phpdoc_type_annotations_only, added in v1.29). Everything else is a stock PHP-CS-Fixer rule name — look those up in the PHP-CS-Fixer docs, not the Pint docs.Sharing one style across many repos
Pint supports extends in pint.json so a central config can be the source of truth:
{
"extends": "https://raw.githubusercontent.com/acme/standards/main/pint.json"
}
Two constraints worth knowing: remote configs must be served over HTTPS (insecure http configs were explicitly rejected in v1.29.2), and local relative paths work too. This is the right pattern for a monorepo or an org with 10+ Laravel services — bump the shared file, let each repo's CI enforce it.
Parallel runs on big repos
./vendor/bin/pint --parallel
Added in v1.23.0 (-p shorthand in v1.24.0) for projects where serial formatting of thousands of files is painful. Use it for full-tree runs; --dirty on a handful of files is already fast enough that parallelism adds noise.
Editor / on-save integration
Two viable patterns:
pint --dirty in a pre-commit hook. One source of truth, zero drift.The first is strongly preferable on a team. Mixed IDE formatters produce exactly the "reformat 400 lines" PRs that make people resent tooling.
AI agent workflows
Recent versions add an agent output format that is auto-selected when Pint detects it is being invoked from Claude Code or OpenCode, with machine-parseable output. If you drive Pint from an agent harness or a script that needs structured results, prefer --format=json (or the agent format) over scraping the human-readable table.
Fix mode exits 0 even when it changed files. The single most common CI mistake: ./vendor/bin/pint in a pipeline never fails. It reformats the ephemeral checkout, discards it, and reports success. Always gate with --test, or opt into --with-exit-status if you actually want fix-and-fail semantics.
--dirty is git-state dependent. It derives its file list from uncommitted changes. In CI this is usually empty — a --dirty step in a pipeline is a no-op that looks like it passes. Also verify behavior in detached-HEAD / shallow-clone environments. Use --dirty locally, --test in CI; do not mix them up.
Stale cache after rule changes. Pint caches which files are already compliant. After editing pint.json, upgrading Pint, or changing presets, a stale cache can mask violations. Run --repair to rebuild it, or delete the cache file. If you have multiple projects sharing a cache location, override it with --cache-file (a matching config key exists too) to avoid cross-contamination.
Rule conflicts are real. Some PHP-CS-Fixer rules fight each other (empty-body braces vs. single-line empty bodies, import ordering vs. fully-qualified strict types). If Pint oscillates — two runs, two different results — you have a conflicting pair. Diagnose by running twice: pint --test twice on a clean tree should both be clean. If not, bisect your rules block.
"preset": "empty" is a trap for the unwary. Setting an empty preset means no rules run unless you list them all explicitly. There was even a historical bug where rules were ignored when empty was specified. Use empty only when you genuinely want to hand-pick every rule; otherwise preset + rules overrides is the correct lever.
Upgrade Pint when you hit flag interactions. Several reported issues were specifically about flags clashing — --parallel combined with --config was broken until v1.27.1, and --parallel dropped verbose output until v1.29.1. If a combination behaves oddly, check the changelog before debugging your config; you may simply be on an older patch.
Errors now go to stderr (v1.29.2). If you parse Pint output in a script, stop assuming everything is on stdout. Route stderr separately or use a structured --format.
Keep it in require-dev. Since v1.26 Composer refuses to install Pint as a non-dev dependency. If you inherited a project with Pint in require, move it — otherwise production installs drag in PHP-CS-Fixer and its transitive tree.
PHP version coupling. Pint is a PHAR/CLI that runs under whatever PHP executes it; its rule set depends on the PHP-CS-Fixer version bundled at that Pint release. If you use PHP 8.4/8.5 syntax (property hooks, asymmetric visibility), pin a Pint version that declares support — 8.5 support was only partial as of v1.26. Formatting a codebase with newer syntax on an older Pint can produce parse errors rather than no-ops.
It is a formatter, not a linter. Pint will not catch unused variables, type errors, or logic problems, and it will happily reformat code that is semantically broken. Pair it with PHPStan/Larastan — the two are complementary, and Pint guarantees the static-analysis diffs stay readable because whitespace churn disappears.
Blade and non-PHP files are out of scope. Pint targets .php. Do not expect it to normalize .blade.php templates; that requires a separate tool.
Extension point: custom fixers. If your team needs a rule Pint does not ship, the path is writing a PHP-CS-Fixer custom fixer and registering it — but that means you are no longer using vanilla Pint, and every upgrade becomes a compatibility exercise. Exhaust rules overrides first; a two-line rules block beats a maintained custom fixer almost every time.
How can I help you explore Laravel packages today?