heyosseus / sloppy
Static analysis for the debt AI coding agents leave behind, on any PHP project: 26 rules, architecture policies and module boundaries you describe, git-diff review, coverage-aware reading order, PHPStan baseline-growth detection, a Rector and Pint fix pass, Pest expectations, CI annotations, agent r
Fund package maintenance!
Requires
- php: ^8.3
- nikic/php-parser: ^5.3
- symfony/console: ^7.0 || ^8.0
- symfony/finder: ^7.0 || ^8.0
- symfony/polyfill-ctype: ^1.27
- symfony/polyfill-mbstring: ^1.27
- symfony/process: ^7.0 || ^8.0
- symfony/yaml: ^7.0 || ^8.0
Requires (Dev)
- illuminate/console: ^12.0 || ^13.0
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^3.7 || ^4.0
- pestphp/pest-plugin-laravel: ^3.1 || ^4.0
- pestphp/pest-plugin-type-coverage: ^3.2 || ^4.0
- rector/rector: ^2.0
Suggests
- ext-xmlreader: Used to read clover and cobertura coverage reports, which raise untested changed files in the review order. Without it coverage is skipped and nothing else changes.
- filament/filament: Required for the Filament panel plugin and health widget.
- heyosseus/phpstan-sloppy: The PHPStan extension: reports Sloppy findings as PHPStan errors, inside your existing PHPStan run and baseline.
- illuminate/console: Required for the php artisan sloppy commands. Not needed for the standalone bin/sloppy binary.
- laravel/pint: Used by sloppy fix to format the files Rector rewrote.
- pestphp/pest: Required for the expectCleanSloppyDiff() test expectation.
- rector/rector: Required by sloppy fix, which hands the fixable findings to Rector.
Provides
None
Conflicts
None
Replaces
None
README
Your AI agent writes the PHP. Sloppy makes it clean up after itself.
It catches the code people regret — god methods, swallowed exceptions, N+1 queries —
inside Claude Code, your CI and your Pest suite.
Deterministic and local: no model, no API key, no network.
Real output. Claude Code runs this after every edit and hands it to the model, which fixes the catch block before you ever see it.
Quick start
composer require --dev heyosseus/sloppy vendor/bin/sloppy # scan the project (php artisan sloppy in Laravel) vendor/bin/sloppy agents install # make Claude Code check its own work
That's it: no configuration file, no account. Sloppy finds your source roots
from composer.json. It runs on any PHP 8.3+ project, and adds ten
Eloquent-aware rules when it finds Laravel 12 or 13.
A first scan of a codebase with history does not hand you hundreds of findings to work through. It lists the defects worth fixing first, sums up the rest per file, and offers to baseline what is already there.
Trying it before adding a dependency? Run composer global require heyosseus/sloppy,
or download the phar.
See Getting started.
How the agent loop works
Agents produce code fast, and they produce the same mistakes fast: the
catch (Throwable) that hides a failure, the 200-line controller action, the
query inside a loop. Code review catches them late. Sloppy catches them while
the agent still has the code in hand.
- The rules go in first.
CLAUDE.md(orAGENTS.md,.cursorrules, Copilot, Windsurf, Laravel Boost) gets this project's rules, each written as an instruction the agent can follow. - Every edit is checked. After each change to a PHP file, Claude Code runs Sloppy and hands the model anything that edit introduced, then the model fixes it.
- It can't finish dirty. When the agent tries to call the task done, new findings at or above your threshold send it back to fix them.
It's built never to get in the way. Findings a file already had are never
reported, so the agent doesn't wander off "fixing" code nobody asked it to
touch. The finish check blocks once, so a false positive costs one round trip,
not the session. And if Sloppy can't run (no git, a broken config), the agent
carries on. sloppy agents uninstall takes it all back out, leaving every
other setting byte for byte as it was.
There's also an MCP server for agents that should scan on demand. See Coding agents.
What it catches
26 rules, plus two checks that compare your change with its base, and five more that enforce an architecture you describe. Each one is tested to fire on the pattern and to stay silent on ordinary Laravel code.
| Rules | |
|---|---|
| Complexity | SL101 God Method · SL102 God Class · SL103 Excessive Nesting |
| Duplication | SL104 Duplicate Logic · SL111 Copy-Paste Drift, a near-copy whose one difference looks like an unfinished edit |
| Dead code & dependencies | SL105 Dead Private Method · SL106 Unused Constructor Dependency · SL112 Placeholder Implementation, the // ... existing code ... or "not implemented" left where a body should be |
| Error handling | SL107 Swallowed Exception |
| Readability | SL108 Redundant Condition · SL109 Narrative Comment · SL110 Defensive Programming Noise |
| Laravel | SL201 Business Logic In Controller · SL202 Inline Validation · SL208 Direct External API Call · SL209 Model Doing Too Much |
| Performance | SL203 Possible N+1 · SL204 Query Inside Loop · SL205 Collection Instead Of Query · SL210 Suspicious Model::all() |
| Dependencies | SL206 Excessive Controller Dependencies · SL207 Excessive Service Dependencies |
| Architecture (advisory) | SL301 Abstraction Inflation · SL302 Empty Wrapper Class · SL303 Single-Use Abstraction |
| Your architecture (opt-in) | SL304 Layer Violation · SL305 Forbidden Capability, a query in a controller or env() in the domain · SL306 Boundary Violation, one module reaching into another's internals · SL307 Misplaced Class, a new class with no place in the architecture · SL308 Role Shape, an action with a second public method |
| Suppression | SL501 Unexplained Suppression · SL502 Baseline Growth, new entries in your PHPStan or Psalm baseline · SL503 Weakened Test, a test skipped, stripped of assertions or given assertTrue(true) to make it pass |
Every finding says where it is, what was measured, how sure the analyser is, why the pattern costs you, and what to do instead. See Rules, or write your own.
Beyond the agent
The same analyser, wherever else you want the answer.
Start with what matters. A first scan of a medium-sized application finds
hundreds of things, and nobody reads hundreds of things. So sloppy lists the
defects first (swallowed exceptions, queries in loops, unfinished bodies),
highest risk first. It sums up the long methods and narrating comments per
file, putting the files that keep changing at the top. Then it tells you which
commands shorten the list without reading it: sloppy fix for the mechanical
findings, sloppy baseline for the debt that is already there. --all lists
everything.
Review a change. sloppy diff main reports only what your branch
introduced, never what it inherited; add --merge-base to compare with
where your branch forked, as sloppy ci does. sloppy review main ranks the same
findings by risk, so you know which file to read first and where to stop.
Adopt it on a codebase with history. sloppy baseline accepts today's
debt, so only new findings fail the build. Entries are keyed on class and
member, not line numbers, so the baseline survives ordinary editing.
Gate it in CI. sloppy ci reads the pipeline it runs in: it annotates the
diff on a GitHub pull request and fills the Code Quality widget on GitLab. The
GitHub Action is three lines:
- uses: heyosseus/sloppy-action@v1 with: diff-branch: main
Already on PHPStan? Get the same findings inside the run you already have:
composer require --dev heyosseus/phpstan-sloppy
Each one is a PHPStan error with its own identifier (sloppy.SL107), so
@phpstan-ignore, ignoreErrors and PHPStan baselines work on it. It reports
exactly what sloppy ci would fail on. See
heyosseus/phpstan-sloppy.
Fail your tests on new debt. A Pest plugin ships with the package:
it('has no new slop on this branch', function (): void { expectCleanSloppyDiff('main'); });
Fix what a machine can fix. sloppy fix hands the mechanical findings to
Rector, scoped to the files that have them, deletes the comments that only
restate their code, then formats with Pint. It tells you which findings still
need a person, and why no tool should touch them.
Keep score while you work. sloppy watch keeps the score and what to read
first on screen, redrawing on every save. It's made to sit beside an agent that
is writing code.
See it where you already look. Use SARIF for GitHub code scanning and your
IDE, --format=github for inline annotations, and Markdown for a PR comment.
There's also a Filament widget, a NativePHP menu-bar label, and
JSON output with a published schema.
See Everyday workflow and CI and code scanning.
Why trust the numbers
It is not an AI detector. Nobody can reliably tell from source code who or
what wrote it, and Sloppy never tries. It detects patterns that turn into
maintenance cost, whoever wrote them. A 200-line controller action that swallows
a Throwable is a problem whether a person or an agent wrote it at 3am.
| Sloppy says | It means | It does not mean |
|---|---|---|
Confidence: 88% |
How sure the analyser is that the pattern is really there | Any probability that the code was AI-generated |
Score: 67/100 |
A code-quality risk measure for the analysed paths | "67% of this code is AI-generated" |
Every number shows its working. Add --explain-risk and each score and risk
prints the arithmetic that produced it. The same code always produces the same
report, which is what makes Sloppy usable as a gate. See
Score, severity and risk.
It is tuned for silence. Rules need several signals, not one; they understand framework conventions; and they back off wherever code could be reached indirectly. Sloppy runs over its own source in CI, and a test asserts that ordinary Laravel code produces zero findings. The rules and the score are checked against eight open-source Laravel applications, 782,578 lines, where the four healthiest score 87 to 90.
It complements your tools rather than replacing them:
| Tool | Answers |
|---|---|
| PHPStan / Psalm | Is this type-correct? |
| Pint / PHP_CodeSniffer | Is this formatted consistently? |
| Pest / PHPUnit | Does this behave correctly? |
| Rector | Can this be transformed mechanically? |
| Sloppy | Is this shaped like code somebody will regret? |
If PHPStan can prove it, Sloppy stays out of it.
Documentation
| Getting started | Install options, every command, your first scan and how a long one is triaged, adopting on an existing codebase |
| Coding agents | Claude Code hooks, rulesets for every agent, Laravel Boost, the MCP server |
| Rules | Every rule in detail, and how false positives are kept down |
| Everyday workflow | Diff and review, sloppy fix over Rector, Pint and narrating comments, Pest expectations, watch, Filament and NativePHP |
| CI and code scanning | sloppy ci, the GitHub Action, GitLab, SARIF, annotations, exit codes |
| Score, severity and risk | How every number is calculated, and what coverage, git history and PHPStan baselines feed |
| Configuration | config/sloppy.php, where the binary looks for it, sloppy-architecture.php, and taming a noisy first run |
| JSON output | The machine-readable report and its contract |
| Custom rules | Writing your own rule, and where it shows up |
Roadmap
Still ahead: inline pull-request review comments, sloppy explain for a
longer write-up of one finding, HTML reports, and more rules for the shortcuts
agents take, such as configuration keys and routes that do not exist. Anything AI-assisted
will be opt-in and separate: the analyser will always work with no API key, no
network and no model.
Contributing
Found a false positive? That's a bug worth reporting, not a threshold to work
around. Open an issue. To work on
Sloppy itself, see CONTRIBUTING.md. composer test runs
Rector, Pint, PHPStan at level 8, 100% type coverage and the suite.
Security issues: see SECURITY.md.
License
MIT. See LICENSE.md.