Skip to content

Laravel Security Audits With the AI Agent You Already Use

Laravel Security Audits With the AI Agent You Already Use

Confession: the first time I asked an AI agent to “audit this Laravel app for security issues,” I printed the report for a client meeting. I’m glad I read it in the car first. Half the findings were style opinions wearing a severity label. One “critical vulnerability” was a perfectly normal use of whereRaw with bound parameters. And the thing that should have been flagged, a destroy route with no policy check, sailed through unmentioned because the agent never actually read the routes file. It read some files, got a vibe, and wrote confident prose about the vibe.

So when Laravel Auditor showed up on Laravel News this week, I was primed to be skeptical and ended up impressed. It attacks the exact failure I hit: agents that guess instead of collecting facts. The short version: the package itself scans nothing. You bolt a methodology and a set of read-only fact collectors onto the agent you already use, whether that’s Claude Code, Codex, Cursor, or half a dozen others, and the agent does the auditing.

The audit I asked for and the slop I got back

The failure mode is worth naming precisely, because it shapes why this package looks the way it does. When you ask a bare agent to audit a Laravel security posture, it tends to do four bad things. It guesses at framework behavior instead of checking what version you’re on and what the framework already handles. It reports style preferences as high-severity issues. It invents vulnerabilities from patterns that merely look unusual. And it skips verification, so a hypothesis gets written up with the same confidence as a confirmed finding.

None of this is because the model is weak. It’s because the agent has no protocol. A human auditor with no checklist and no access to the schema would produce similar junk, just slower.

What Laravel Auditor actually installs

The package, by Punyapal Shah, is a dev dependency that executes no checks itself. What it ships is a catalog of 75 evidence-first rules with stable IDs, from AUD-SEC-001 (missing authorization boundary) through AUD-PER-011 (query executed inside a loop), plus skills and guidelines it publishes into your agent’s native format, plus a local MCP server exposing read-only context collectors.

Setup is two commands:

composer require --dev mrpunyapal/laravel-auditor

php artisan auditor:install --agents=claude_code

If you’re already on Laravel Boost, boost:install wires it through Boost’s third-party skills instead and you skip the second command. Either way you then open your agent and ask it to use the laravel-audit skill.

The rule packs are conditional, which I appreciate. Livewire, Filament, Inertia, Sanctum, and Pest rules only activate when those packages are actually installed, so you don’t get findings about a stack you don’t run. It needs PHP 8.3+ and Laravel 12 or 13, and it’s an early 0.1.x, so temper expectations accordingly. The author is upfront that finding quality depends on the agent following the skill.

Facts before code: the context collectors

The piece that fixes my car-ride problem is the collectors. Before the agent reads a line of your source, it can pull deterministic facts over MCP or Artisan: project_info for versions and ecosystem signals, routes for the full route surface with middleware, models with fillable, guarded, casts, and relationships, database_schema for actual tables and indexes, policies_authorization for gates and policies, and six more covering migrations, dependencies, config, jobs, tests, and subsystem ownership.

That inverts the usual order. My botched audit happened because the agent formed opinions from a sample of files. With collectors, the agent starts from an inventory: here are all 214 routes, here are the 12 that mutate state without auth middleware, now go read those controllers. The workflow the skill enforces is Discover, then Scope, then Verify, then Report, and the skill tells the agent to go deep on the domains with real risk signal instead of skimming everything.

You can also run any collector yourself without an agent involved:

php artisan auditor:context routes --output=storage/auditor-routes.json

I’ve started doing that on inherited codebases even outside audits. A clean JSON dump of every route and model relationship is a better orientation document than most READMEs I inherit.

What a finding looks like when it has to show evidence

The output format is where the discipline shows. Findings are structured JSON with severity, confidence, and mandatory evidence pointing at files and line numbers:

{
  "rule_id": "AUD-SEC-001",
  "title": "Missing authorization boundary",
  "severity": "high",
  "confidence": "confirmed",
  "summary": "Any authenticated user can delete another user's post.",
  "evidence": [
    {
      "type": "file",
      "reference": "app/Http/Controllers/PostController.php",
      "line": 42
    }
  ],
  "recommendation": "Authorize the deletion with a PostPolicy or route middleware."
}

Two fields do the heavy lifting. confidence forces the agent to separate confirmed findings from hypotheses, which directly targets the invented-vulnerability problem. And evidence means a finding with no file reference is visibly incomplete. The skill also instructs the agent to verify severity claims before reporting and to keep style opinions out of high-severity findings entirely.

The performance rules go a step further and require semantic checks before flagging. AUD-PER-008 only flags a materialized ->get()->count() when the collection has no other consumer, because rewriting one that’s also rendered would add a second query and make things worse. That’s the same discipline I argued for in my post on catching N+1 queries in Laravel: the fix has to be verified against how the data is actually used, or the “optimization” is a regression with better branding.

Reports render however you need them:

php artisan auditor:report --findings=storage/auditor-findings.json
php artisan auditor:ci --findings=storage/auditor-findings.json --fail-on=high

That second command turns findings into an exit code, which means the audit can sit in CI and block a merge on any high-severity finding. SARIF output is there too if you want findings in GitHub’s security tab.

Where this fits next to PHPStan and friends

A fair question is why you’d want this when PHPStan, Psalm, and Pint exist. I don’t think it competes with them. Static analyzers are precise about what they can prove and silent about everything else. No PHPStan level will tell you that your delete route trusts any authenticated user, because that’s not a type error. It’s a judgment about intent, and judgment is the part the agent supplies.

The honest caveat cuts the other way: an agent’s judgment is only as reliable as its discipline, and this package is discipline-as-code, not a guarantee. I’d still treat the output as a ranked list of leads for a human to confirm, not a verdict. For Laravel security best practices, the boring baseline still matters more than any tool: policies on every mutating route, $fillable over $guarded, no secrets in the repo, and form requests doing real validation. The auditor is a way to check that baseline held while the codebase grew, which is exactly the kind of thing that slips on the agency projects I take on. I wrote about giving agents better visibility into Laravel apps in my look at the Laravel AI SDK, and this package feels like the same idea pointed at code review instead of runtime.

If you already run static analysis, the two layer sensibly. Let PHPStan clean up everything provable first, at whatever level your codebase tolerates, so the agent isn’t spending its attention on undefined variables and wrong return types. Then point the auditor at what remains: authorization, data exposure, schema drift, the places where correct types can still add up to wrong behavior. Running them in that order also keeps the agent’s context lean, and on a codebase of any size that translates directly into fewer tokens burned re-reading files the type checker already vouched for.

One more practical note on cost. A full audit pass over a mid-sized app means the agent reads a lot of routes, models, and controllers, and if you’re paying per token that adds up. The scoped-domain approach helps here too. A security-only pass over the routes the collectors flagged is a fraction of the reading, and in my experience the findings-per-token ratio is dramatically better than a whole-codebase sweep.

It’s read-only by design, which matters more than it sounds. The collectors return structured facts, never raw source dumps, and the skill instructs the agent not to modify application code. Automatic fixes are explicitly deferred to future versions. Given how much damage an overeager agent can do with write access, starting read-only is the right call for a 0.1.x.

The prompt I’d run this week

If you have a Laravel app and any of the supported agents, the experiment costs maybe twenty minutes. Install the package, wire your agent, and start with the Discover-only pass the docs suggest: have the agent run all eleven collectors, summarize what the app actually is, and flag immediate red flags in five bullets, with no findings written yet. That gives you a cheap read on whether the agent is following the methodology before you trust it with a full audit.

Then run the real thing scoped to one domain. Security first, in my order of operations as a working Laravel developer, because an authorization gap costs you a client and a performance issue only costs you milliseconds. Read every finding against its evidence, throw out anything below confirmed confidence, and fix what survives. If your report comes back half noise, that’s still a better ratio than my car-ride audit, and at least this one shows its work.