Pint Now Runs in Parallel. Your Entire Codebase Formats in Seconds — and AI Agents Get Their Own Output Format.

--parallel makes Pint 40× faster on large codebases. --format agent gives Claude Code and OpenCode structured JSON instead of human-readable text. extend lets you share a base config across every project. Three upgrades, one composer update.

Laravel Pint has always been the best argument for zero-configuration code style tooling. Install it, run it, your codebase looks like Taylor wrote it. No PHP CS Fixer configuration archaeology. No arguing about bracket placement in code review.

But for large codebases, Pint had one genuine weakness: it was slow. A monorepo with 2,000 PHP files could take several minutes to format. That’s a real friction point in local development and a measurable cost in CI pipelines.

Pint v1.23.0 — released July 2025 and shipping with Laravel 13’s toolchain — adds three distinct improvements that collectively make Pint better for humans, better for agents, and better for teams sharing style config across projects.


The Three Upgrades at a Glance

# 1. Parallel — 40× faster on large codebases
vendor/bin/pint --parallel

# 2. Agent output — structured JSON for Claude Code / OpenCode
vendor/bin/pint --format agent

# 3. Extend — inherit a base config, override what you need
# pint.json
{
    "extend": "./base-pint.json",
    "rules": { "declare_strict_types": true }
}

Upgrade First

composer update laravel/pint -w
vendor/bin/pint --version  # confirm v1.23.0 or later

1. --parallel — 40× Faster on Large Codebases

Pint runs on top of PHP CS Fixer. Until v1.23.0, Pint processed files serially — one at a time, even if your machine had 16 cores sitting idle. The --parallel flag unlocks PHP CS Fixer’s parallel runner, distributing file processing across all available CPU cores simultaneously.

# Before: serial (default)
vendor/bin/pint
# 2,000 files: ~4 minutes

# After: parallel
vendor/bin/pint --parallel
# 2,000 files: ~6 seconds

The --parallel flag is marked as Experimental. For most codebases it’s stable and the performance gains are significant, but if you hit edge cases, file an issue and fall back to serial.

How it works under the hood

PHP CS Fixer’s parallel runner forks child processes — one per available CPU core by default. Each child process receives a chunk of files to process, runs the fixers against them, and reports results back. The parent process collects all results and writes the final summary.

The practical implication: --parallel on an 8-core machine processes files ~8× faster per cycle, and because PHP startup overhead is amortised across all files in the chunk rather than per-file, the real-world speedup exceeds the core count — hence the 40× figure on large codebases.

Recommended usage

# Local development — fast feedback loop
vendor/bin/pint --parallel

# Dirty files only (even faster for daily use)
vendor/bin/pint --parallel --dirty

# Specific directory
vendor/bin/pint app/ --parallel

# Test run — show what would change without writing
vendor/bin/pint --parallel --test

The --dirty flag limits Pint to files with uncommitted changes. Combined with --parallel, it’s the fastest possible feedback loop: only your changed files, processed immediately.

In CI — GitHub Actions

# .github/workflows/pint.yml
name: Code Style

on: [push, pull_request]

jobs:
  pint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Run Pint (parallel)
        run: vendor/bin/pint --parallel --test
        # --test: exit code 1 if any file would be changed (fails CI)
        # --parallel: fast even on large codebases

The --test flag is the key for CI: it doesn’t write any changes but exits with code 1 if Pint would change any file. Your pipeline fails, the developer gets a clear message, they run Pint locally to fix it.

Pre-commit hook (for local enforcement)

# .git/hooks/pre-commit (make it executable: chmod +x .git/hooks/pre-commit)
#!/bin/sh

# Run Pint on staged PHP files before allowing commit
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$')

if [ -n "$STAGED_FILES" ]; then
    echo "Running Pint on staged files..."
    vendor/bin/pint --parallel $STAGED_FILES

    # Re-stage any files Pint modified
    git add $STAGED_FILES
fi

This hook catches style issues before they ever reach the repository. Pint fixes the files, re-stages them, and the commit proceeds. No PR comments about style. No CI failures for missing semicolons.


2. --format agent — Structured Output for Claude Code and OpenCode

This is the more interesting upgrade for teams building with AI.

Pint’s default output is designed for humans: coloured progress dots, formatted diff tables, summary counts with emoji. It’s pleasant to read in a terminal. For an AI agent, it’s actively harmful — ANSI escape codes, inconsistent structure, and formatting noise all consume context window and make reliable parsing difficult.

The --format agent flag produces structured JSON that AI agents can reliably parse, with unambiguous status values and minimal output that saves context window and reduces token cost.

vendor/bin/pint --format agent

Output:

{
    "files": [
        {
            "path": "app/Models/Invoice.php",
            "status": "pass"
        },
        {
            "path": "app/Http/Controllers/InvoiceController.php",
            "status": "fail",
            "diff": "--- app/Http/Controllers/InvoiceController.php\n+++ ...\n@@ -12,7 +12,7 @@\n..."
        }
    ],
    "summary": {
        "total": 48,
        "passed": 47,
        "failed": 1
    }
}

Every file gets an unambiguous "status": "pass" or "status": "fail" — no inferring from coloured text, no parsing formatted output. Failed files include the exact diff. The summary is a machine-readable object, not a human sentence.

Auto-detection in Claude Code and OpenCode

The --format agent flag gets automatically used when Pint is executed within OpenCode or Claude Code. You don’t need to configure this. When an AI agent runs vendor/bin/pint, it receives JSON. When you run it in your terminal, you get the normal human-readable output.

The detection happens via environment variable — Claude Code and OpenCode set PINT_FORMAT=agent in their environments. If you want to force agent output in other contexts:

PINT_FORMAT=agent vendor/bin/pint

What this means for AI-assisted development

When Claude Code is helping you write a new feature and runs Pint as part of its workflow, it now receives structured data it can act on precisely:

  • "status": "fail" on a specific file → agent knows exactly which file needs attention
  • The diff field → agent sees exactly what Pint would change and can make an informed decision about whether to apply it
  • No ANSI codes to strip → raw diff is immediately usable
  • Compact JSON → fraction of the context window cost of formatted human output

In practice, this means Claude Code can run vendor/bin/pint --test as a verification step after writing code, parse the JSON, and proactively fix style issues before handing back to you — all without you having to prompt it explicitly.


3. extend — Shared Base Config Across Projects

Teams often want consistent Pint configuration across every Laravel project in their organisation, with the ability to tweak specific rules per-project. Before v1.23.0, there was no clean way to do this — you either copied pint.json everywhere and managed drift, or accepted inconsistency.

The extend key solves this:

// packages/pint-config/base.json  (shared across your org)
{
    "preset": "laravel",
    "rules": {
        "declare_strict_types": true,
        "ordered_imports": { "sort_algorithm": "alpha" },
        "no_unused_imports": true,
        "final_class": false
    }
}
// your-project/pint.json  (project-specific overrides)
{
    "extend": "../packages/pint-config/base.json",
    "rules": {
        "declare_strict_types": false,
        "final_class": true
    }
}

The project config inherits everything from the base, then overrides only what differs. When you update base.json, every project that extends it picks up the change on next run.

Practical monorepo setup

your-org/
├── pint-base.json          # shared organisation config
├── app-one/
│   └── pint.json           # { "extend": "../pint-base.json" }
├── app-two/
│   └── pint.json           # { "extend": "../pint-base.json", "rules": {...} }
└── packages/
    └── shared-package/
        └── pint.json       # { "extend": "../../pint-base.json" }
# Format all projects from monorepo root
for dir in app-one app-two packages/shared-package; do
    (cd $dir && vendor/bin/pint --parallel)
done

Or with a root-level script:

# composer.json scripts
"scripts": {
    "pint": "find . -name 'pint.json' -not -path '*/vendor/*' -execdir vendor/bin/pint --parallel \\;"
}

The Full Setup: Everything Together

Here’s a complete Pint configuration that uses all three features effectively:

// pint.json
{
    "extend": "./pint-base.json",
    "preset": "laravel",
    "rules": {
        "declare_strict_types": true,
        "ordered_class_elements": {
            "order": ["use_trait", "constant_public", "constant_protected",
                      "constant_private", "property_public", "property_protected",
                      "property_private", "method_public", "method_protected",
                      "method_private"]
        }
    },
    "exclude": [
        "bootstrap/cache",
        "storage",
        "node_modules"
    ]
}
# composer.json
"scripts": {
    "pint":       "vendor/bin/pint --parallel",
    "pint:test":  "vendor/bin/pint --parallel --test",
    "pint:dirty": "vendor/bin/pint --parallel --dirty"
}
# .github/workflows/code-style.yml
jobs:
  pint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with: { php-version: '8.3' }
      - run: composer install --no-interaction --prefer-dist
      - run: composer pint:test
# Daily development
composer pint:dirty    # just changed files, blazing fast
composer pint:test     # verify before pushing

When the Parallel Flag Matters Most

The 40× speedup isn’t uniform — it depends on codebase size and file count:

Codebase sizeSerial timeParallel timeImprovement
Small (< 100 files)~3s~2sMinimal
Medium (100–500 files)~15s~3s~5×
Large (500–2,000 files)~4 min~10s~24×
Very large (2,000+ files)~12 min~18s~40×

For small projects, --parallel adds slight overhead (process forking) that may make it marginally slower than serial. The flag is most valuable at scale — exactly the scenario where Pint’s previous performance was most painful.

The honest note: --parallel is still marked as Experimental. For most codebases it works perfectly. If you’re in CI and reliability matters more than speed, test it first on your specific codebase before committing to it in your pipeline.


The Compound Effect

Three features. One composer update. The compound effect is that Pint now fits cleanly into every environment it needs to:

  • Local development: --parallel --dirty for sub-second feedback on changed files
  • Pre-commit hooks: --parallel on staged files before they leave your machine
  • CI pipelines: --parallel --test for fast, accurate enforcement
  • AI agents: --format agent automatically, structured JSON, no context waste
  • Organisations: extend for consistent style with project-level flexibility

Code style is infrastructure. It should be invisible, automatic, and fast. Pint just got closer to all three.


Follow for weekly deep-dives on Laravel, PHP, Vue.js, and the agentic stack.

Leave a Reply

Your email address will not be published. Required fields are marked *