Search by

limenet / laravel-baseline

limenet

A highly opinionated Laravel baseline.

Package info

github.com/limenet/laravel-baseline

pkg:composer/limenet/laravel-baseline

Fund package maintenance!

Linus Metzler

Statistics

Installs: 1 368

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 1

2.17.0 2026-10-08 12:24 UTC

README

Latest Version on Packagist run-tests Fix PHP code style issues codecov Total Downloads

Checks your Laravel installation against a highly opinionated baseline.

This repository ships two runners from one policy:

Composer (Laravel) Composer (standalone) npm
Package limenet/laravel-baseline limenet/laravel-baseline @limenet-ch/baseline
For Laravel projects (DDEV, composer) other PHP projects, e.g. WordPress themes JS/TS-only projects (no PHP, no DDEV)
Command php artisan limenet:laravel-baseline:check vendor/bin/baseline check npx baseline check
Checks all of them the php / wordpress profiles the portable subset

Both read policy/, so the version floors and required keys are defined once, and both are executed against the shared behavioural fixtures in fixtures/. They are released in lockstep: the same version number means the same policy in both ecosystems.

Installation

You can install the package via composer:

composer require limenet/laravel-baseline

You can publish the config file with:

php artisan vendor:publish --tag="laravel-baseline-config"

Usage

Add to your composer.json to run checks (and auto-fix) after every composer update:

"post-update-cmd": [
    "@php artisan limenet:laravel-baseline:check --fix"
],

Running checks

# Check only β€” report issues without making changes
php artisan limenet:laravel-baseline:check

# Auto-fix β€” apply all safe automatic fixes, then report remaining issues
php artisan limenet:laravel-baseline:check --fix

Checks marked πŸ”§ below support --fix. When --fix is used:

  • Fully fixable checks: all conditions are applied automatically.
  • Partially fixable checks (requires package installed first): configuration/script entries are fixed once the package is installed via composer require.
  • Non-fixable checks: report the issue with an actionable message.

AI guidelines & skills

The package also ships Laravel Boost resources under resources/boost/: an always-on guideline (the dev loop β€” ci-lint, tests, DDEV-first conventions) and on-demand skills (e.g. creating-a-release). When a project that has laravel/boost installed runs php artisan boost:install or php artisan boost:update --discover, Boost discovers and publishes these to the consuming project's coding agents automatically.

Non-Laravel PHP projects

The same composer package also checks PHP projects that are not Laravel apps, through vendor/bin/baseline instead of artisan. It runs the checks that make sense without Laravel β€” composer scripts, PHPStan, Pint, plain Rector sets, DDEV, CI, editor and Claude settings β€” and skips everything that needs artisan, config/*.php or a Laravel package.

composer require --dev limenet/laravel-baseline
ddev exec vendor/bin/baseline check              # report issues
ddev exec vendor/bin/baseline check --fix        # apply safe fixes, then report what is left
ddev exec vendor/bin/baseline periodic           # walk through expired periodic checks

Run it after every composer update, like the Laravel runner:

"post-update-cmd": [
    "@php vendor/bin/baseline check --fix"
],

The runner picks a profile from the project:

Profile Detected by
wordpress a style.css with a Theme Name: header, a root *.php with a Plugin Name: header, or a composer type of wordpress-theme / wordpress-plugin / wordpress-muplugin
php anything else

A Laravel application (an artisan file, or laravel/framework in require) is refused β€” use the artisan command there, which runs the full Laravel profile. Set "profile": "php" or "profile": "wordpress" in .baseline.json to override the detection.

Outside Laravel a test suite is optional: the Pest, phpunit.xml and pcov checks report a warning instead of failing until pestphp/pest is installed, and the CI test job is not required.

State lives in .baseline.json at the project root, the same file and shape the npm runner uses:

{
    "excludes": ["hasTrivyConfig"],
    "periodic": { "updatesDependencies": "2026-08-16T09:00:00+00:00" }
}

What the standalone runner checks

Every check below also runs in Laravel projects unless marked standalone-only.

Check php wordpress Relationship to the Laravel runner
allowsToolingInClaudeSettings βœ“ βœ“ requires the shared and ddev composer allow entries, not the artisan ones
biomeIgnoresCiArtifacts βœ“ βœ“ identical
biomeUsesLocalSchema βœ“ βœ“ identical
bumpsComposer βœ“ βœ“ identical
callsBaseline βœ“ βœ“ hooks @php vendor/bin/baseline check --fix into post-update-cmd instead of the artisan command
checkPhpunit βœ“ βœ“ warns until a test suite exists; requires only the cobertura and JUnit reports, not APP_KEY or the ./app source
ddevHasPcovPackage βœ“ βœ“ warns until pestphp/pest is installed
ddevMutagenIgnoresNodeModules βœ“ βœ“ identical
ddevNodeVersionIsAuto βœ“ βœ“ identical
deniesEnvReadsInClaudeSettings βœ“ βœ“ identical
doesNotCallPeriodicBaselineOnUpdate βœ“ βœ“ flags vendor/bin/baseline periodic instead of the artisan command
doesNotExcludeUnknownChecks βœ“ βœ“ reads .baseline.json, against this profile’s checks
doesNotHaveCopilotOrJunieAgentFiles βœ“ βœ“ identical
doesNotUseBothBaselineRunners βœ“ βœ“ identical
doesNotUseGreaterThanOrEqualConstraints βœ“ βœ“ identical
doesNotUsePhpCsFixer βœ“ βœ“ identical
doesNotUsePhpInsights βœ“ βœ“ identical
hardensNpmSupplyChain βœ“ βœ“ identical
hasCiJobs βœ“ βœ“ same GitLab CI templates, without the test job
hasEditorconfig βœ“ βœ“ identical
hasNpmScripts βœ“ βœ“ identical
hasRectorConfigWithAttributesSets βœ“ βœ“ identical
hasRectorConfigWithImportNames βœ“ βœ“ identical
hasRectorConfigWithPestSet βœ“ βœ“ identical
hasRectorConfigWithPhpSets βœ“ βœ“ identical
hasRectorConfigWithPreparedSets βœ“ βœ“ identical
hasTrivyConfig βœ“ βœ“ identical
isCiLintComplete βœ“ βœ“ identical
isInstalledAsDevDependency βœ“ βœ“ standalone-only: the package belongs in require-dev; the Laravel runner's isInstalledAsRegularDependency requires require instead, because a Laravel app loads it at runtime
nodeVersion βœ“ βœ“ identical
phpVersionMatchesCi βœ“ βœ“ identical
phpVersionMatchesDdev βœ“ βœ“ identical
phpstanCoversAllPhpFiles βœ“ βœ“ standalone-only
phpstanLevelAtLeastEight βœ“ βœ“ identical
rectorCoversAllPhpFiles βœ“ βœ“ standalone-only
releaseItBumpsWordpressThemeVersion βœ“ standalone-only, themes only
runsCiLintHookInClaudeSettings βœ“ βœ“ identical
updatesDdevAddons βœ“ βœ“ identical
updatesDependencies βœ“ βœ“ identical (periodic, every 30 days); recorded in .baseline.json
usesPest βœ“ βœ“ warns until pestphp/pest is installed; does not require pest-plugin-laravel
usesPestPhpstanPlugin βœ“ βœ“ identical
usesPestRectorPlugin βœ“ βœ“ identical
usesPhpstanExtensions βœ“ βœ“ identical
usesPhpstanWordpress βœ“ standalone-only
usesRector βœ“ βœ“ requires rector/rector only, not driftingly/rector-laravel
usesReleaseIt βœ“ βœ“ identical
wordpressThemeVersionMatchesComposer βœ“ standalone-only, themes only

Deliberately not run outside Laravel: everything artisan-, config/*.php-, schedule- or Laravel-package-shaped (Horizon, Pulse, Telescope, Spatie Health, Boost, IDE helpers, Larastan, rector-laravel), and hasRectorConfigWithPaths / hasRectorConfigWithComposerBased, whose values assume Laravel's layout β€” rectorCoversAllPhpFiles and phpstanCoversAllPhpFiles take their place.

JS-only projects

Projects with no PHP and no DDEV use the npm runner instead. It is a second implementation, not a wrapper: PHP cannot reach into a JS project (and by this baseline's own convention npm runs on the host while artisan runs inside DDEV), so the portable checks are reimplemented in TypeScript and kept honest by the shared fixtures rather than by shared code.

npm install --save-dev @limenet-ch/baseline
npx baseline check              # report issues
npx baseline check --fix        # apply safe fixes and install/update the skills, then report what is left
npx baseline periodic           # walk through expired periodic checks
npx baseline install-skills     # copy the packaged skills into .claude/skills/ without running the checks

Wire it into the ci-lint npm script so CI and the Claude Stop hook both run it β€” npm has no post-update-cmd equivalent, and npm 12 blocks dependency lifecycle scripts by default:

"scripts": {
    "ci-lint": "biome ci . && tsc --noEmit && baseline check"
}

State lives in .baseline.json at the project root (a JS project has no config/ directory):

{
    "excludes": ["hasNpmScripts"],
    "periodic": { "updatesDependencies": "2026-08-16T09:00:00.000Z" }
}

What the npm runner checks

Check Relationship to the Laravel runner
nodeVersion identical
hardensNpmSupplyChain also requires .npmrc to exempt @limenet-ch/baseline from the cooldown (min-release-age-exclude[]=@limenet-ch/baseline, or a glob covering it), so a baseline fix lands without the 7-day wait
hasEditorconfig identical
biomeUsesLocalSchema identical
biomeIgnoresCiArtifacts identical
doesNotHaveCopilotOrJunieAgentFiles identical
doesNotUseBothBaselineRunners mirrored: fails when composer.json requires limenet/laravel-baseline, since the Composer runner wins
allowsToolingInClaudeSettings requires only the shared allow entries, not the DDEV/artisan ones
deniesEnvReadsInClaudeSettings identical
runsCiLintHookInClaudeSettings hooks npm run ci-lint instead of ddev composer run ci-lint
updatesDependencies identical (periodic, every 30 days)
hasNpmScripts identical
hasCiJobs same GitLab CI templates, without the php job
hasTrivyConfig identical, canonical config included: its vendor/**, storage/logs/ and .ddev/ skips are inert in a JS project
ciSetsNodeVersion npm-only: the Laravel runner does not register it
isCiLintComplete asserts the JS toolchain in the npm script, not pint/phpstan in a composer script: whichever linter is installed (biome for @biomejs/biome, eslint for eslint), failing if neither is, plus tsc once the project has a .ts/.tsx/.mts/.cts file outside node_modules and dot-directories
callsBaseline hooks the ci-lint npm script, since npm has no post-update-cmd
doesNotExcludeUnknownChecks identical, against this runner's smaller registry: a name only the Laravel runner knows is as dead here as one that exists nowhere
usesReleaseIt inverted: fails if @release-it/bumper is configured, because package.json is already release-it's source of truth

Deliberately not ported: everything composer-, artisan-, Rector-, PHPStan- or Spatie-Health-shaped; the DDEV checks (ddevNodeVersionIsAuto, ddevMutagenIgnoresNodeModules, …); and hasClaudeSettingsWithLaravelSkills / doesNotHaveLaravelSimplifierInClaudeSettings, which are vacuous without Laravel.

Checks

This package validates your Laravel installation against the following checks:

Testing & Quality Tools

  • usesPest() - Validates Pest testing framework is configured (not PHPUnit directly)
  • usesPestPhpstanPlugin() - Validates pestphp/pest-plugin-phpstan is installed when Pest 5+ and PHPStan are both present (warns if not applicable)
  • usesPestRectorPlugin() - Validates pestphp/pest-plugin-rector is installed when Pest 5+ and Rector are both present (warns if not applicable)
  • πŸ”§ usesRector() - Validates Rector automated code modernization is installed, with driftingly/rector-laravel constrained to at least ^2.6.1 β€” the release where LaravelSetProvider is gone and its rules arrive through LaravelSetList::COMPOSER_BASED instead (partial: fixes ci-lint script if packages installed)
  • usesLarastan() - Validates Larastan static analysis tool is configured
  • usesPhpstanExtensions() - Validates PHPStan extensions are installed
  • usesPhpstanWordpress() - Validates szepeviktor/phpstan-wordpress is installed, so PHPStan knows WordPress's functions, classes and hooks (wordpress profile only)
  • phpstanCoversAllPhpFiles() - Validates PHPStan's parameters.paths cover every PHP file the project owns (outside vendor/, node_modules/, dot-directories and git-ignored paths); files listed in excludePaths count as deliberately skipped. includes are not followed (php and wordpress profiles)
  • phpstanLevelAtLeastEight() - Validates PHPStan is configured to at least level 8
  • πŸ”§ phpstanParsesModelCastsMethod() - Validates phpstan.neon sets parseModelCastsMethod: true: ModelCastsPropertyToCastsMethodRector rewrites protected $casts = [...] into a casts(): array method, and without this parameter Larastan reads only the generated @return array<string, string> β€” not a constant array β€” so every cast is lost and datetime attributes report as strings (inserts the parameter into the parameters block)
  • πŸ”§ checkPhpunit() - Validates PHPUnit configuration with coverage reports (adds missing XML nodes and APP_KEY)
  • πŸ”§ hasRectorConfigWithComposerBased() - Validates Rector withComposerBased(phpunit, symfony, laravel) is configured (appends call to rector.php)
  • πŸ”§ hasRectorConfigWithConfiguredRules() - Validates Rector withConfiguredRule() calls are present for RouteActionCallableRector and WhereToWhereLikeRector (appends calls to rector.php)
  • πŸ”§ hasRectorConfigWithPreparedSets() - Validates Rector withPreparedSets(deadCode, codeQuality, codingStyle, typeDeclarations, privatization, instanceOf, earlyReturn) is configured (appends call to rector.php)
  • πŸ”§ hasRectorConfigWithImportNames() - Validates Rector withImportNames(importShortClasses: false) is configured (appends call to rector.php)
  • πŸ”§ hasRectorConfigWithPhpSets() - Validates Rector withPhpSets() is called (appends call to rector.php)
  • πŸ”§ hasRectorConfigWithAttributesSets() - Validates Rector withAttributesSets() is called (appends call to rector.php)
  • πŸ”§ hasRectorConfigWithRules() - Validates Rector withRules([MinutesToSecondsInCacheRector, UseForwardsCallsTraitRector]) is configured (appends call to rector.php)
  • πŸ”§ hasRectorConfigWithSets() - Validates Rector withSets([LaravelBaselineSetList::REMOVE_DEFAULT_DOCBLOCKS, LaravelSetList::LARAVEL_*]) is configured with all required sets (appends call to rector.php)
  • πŸ”§ rectorCoversAllPhpFiles() - Validates rector.php's withPaths() (plus withRootFiles()) cover every PHP file the project owns (outside vendor/, node_modules/, dot-directories and git-ignored paths); paths in withSkip() count as deliberately skipped (php and wordpress profiles; adds ->withRootFiles() when only root files are uncovered β€” which directories to process is left to the developer)
  • πŸ”§ hasRectorConfigWithPaths() - Validates Rector withPaths([app, database, routes, tests]) is configured (appends call to rector.php)
  • πŸ”§ hasRectorConfigWithPestSet() - Validates Rector withSets([PestSetList::CODING_STYLE]) is configured when Pest 5+ and Rector are both present (appends call to rector.php; warns if not applicable)
  • πŸ”§ hasRectorConfigWithSkip() - Validates Rector withSkip() contains required skipped rules (always: 6 Laravel rules plus StringToClassConstantRector, which maps the Laravel 5.2-era string events context-free and so rewrites any matching literal β€” view('auth.login') becomes Illuminate\Auth\Events\Login::class; Laravel 13+: TablePropertyToTableAttributeRector; AddGenericBuilderToScopesRector, new in rector-laravel 2.6 and shipped in LARAVEL_TYPE_DECLARATIONS, which downgrades an already-correct Builder<$this> to Builder<static>; MigrateToSimplifiedAttributeRector, which rewrites working getFooAttribute()/setFooAttribute() accessors into a single Attribute method; when server.php exists: ServerVariableToRequestFacadeRector) (appends an imported withSkip() call, or merges the missing classes into one that already exists)

IDE & Developer Tools

  • πŸ”§ hasEditorconfig() - Validates .editorconfig exists with required settings (root = true, charset, end_of_line, indent_style, insert_final_newline, trim_trailing_whitespace) (creates .editorconfig with canonical content if missing, and adds missing properties to [*] while keeping the project's own sections if incomplete)
  • πŸ”§ hasClaudeSettingsWithLaravelSkills() - Validates Claude Code settings include the Laravel agent skills plugin and marketplace (creates/merges .claude/settings.json)
  • πŸ”§ doesNotHaveLaravelSimplifierInClaudeSettings() - Fails if the deprecated laravel-simplifier@laravel plugin is still enabled in .claude/settings.json (removes the entry)
  • πŸ”§ deniesEnvReadsInClaudeSettings() - Validates .claude/settings.json permissions.deny blocks reading .env plus every environment that ships an encrypted file (each .env.{env}.encrypted in the project root requires denying .env.{env}); .env.example stays readable (merges the deny entries)
  • πŸ”§ allowsToolingInClaudeSettings() - Validates .claude/settings.json permissions.allow includes the DDEV dev-loop commands (ddev composer run ci-lint, ddev composer test, and safe artisan commands: test, make:*, route:list, about, config:show, ide-helper, optimize:clear, cache:clear, config:clear, route:clear, view:clear) so the dev loop runs without prompts (merges the allow entries)
  • πŸ”§ asksBeforeDestructiveDbCommandsInClaudeSettings() - Validates .claude/settings.json permissions.ask requires a confirmation before the artisan commands that destroy database contents (migrate:fresh, migrate:refresh, migrate:reset, migrate:rollback, db:wipe), covering both the ddev artisan and php artisan forms; ask rather than deny so the developer keeps an approval path (merges the ask entries)
  • πŸ”§ runsCiLintHookInClaudeSettings() - Validates .claude/settings.json has a Stop hook running ddev composer run ci-lint (appends the hook)
  • πŸ”§ usesIdeHelpers() - Validates Laravel IDE Helper is configured: post-update-cmd runs ide-helper:generate, ide-helper:models, and ide-helper:meta, and .gitignore ignores the generated _ide_helper.php, _ide_helper_models.php, and .phpstorm.meta.php files (partial: adds post-update scripts and gitignore entries if package installed)
  • πŸ”§ gitignoresLspFiles() - Validates .gitignore ignores storage/framework/lsp-*.php, the per-editor files the Laravel language server writes into storage/framework (appends the entry, creating .gitignore if missing)
  • usesLaravelAdminer() - Warns if Laravel Adminer database UI is missing (optional), validates TFA confirmation and configuration when installed
  • πŸ”§ usesLaravelBoost() - Validates Laravel Boost AI development tool (partial: fixes boost.json and post-update script if package installed)
  • πŸ”§ laravelBoostMcpUsesDdev() - Validates .mcp.json configures the laravel-boost MCP server to use ddev artisan boost:mcp (creates/fixes .mcp.json; warns if laravel/boost not installed)
  • πŸ”§ doesNotHaveCopilotOrJunieAgentFiles() - Fails if AGENTS.md, a .junie directory, or a .github/skills directory exist β€” these are generated for the Copilot/Junie Boost agents, which are no longer required (deletes AGENTS.md, the .junie directory, and the .github/skills directory)
  • runsBoostUpdate() (periodic, every 30 days) - Warns if Laravel Boost is not installed; when installed, fails until a developer confirms running php artisan boost:update --discover via limenet:laravel-baseline:periodic
  • followsModernLaravelIdioms() (periodic, every 30 days) - Fails until a developer confirms running the auditing-laravel-idioms skill, which audits typed cache getters, BackedEnum cache/session keys, and whenFilledEnum() for request data
  • updatesDependencies() (periodic, every 30 days) - Fails until a developer confirms (via limenet:laravel-baseline:periodic) that composer & npm dependencies were updated by running the updating-dependencies skill β€” which updates in-constraint packages, reviews changelogs for project impact, and reports semver-blocked majors
  • πŸ”§ usesLimenetPintConfig() - Validates custom Laravel Pint formatting standards (partial: adds post-update script if package installed)

Laravel Features & Monitoring

  • πŸ”§ usesLaravelHorizon() - Validates Laravel Horizon Redis queue manager (partial: adds ci-deploy-post script if package installed)
  • usesLaravelPennant() - Warns if Laravel Pennant feature flags are missing (optional)
  • πŸ”§ usesLaravelPulse() - Validates Laravel Pulse application monitoring (partial: adds PULSE_ENABLED=false to phpunit.xml if package installed)
  • πŸ”§ cacheAllowsPulseSerializableClasses() - On Laravel 13+ with Pulse installed, validates the top-level serializable_classes allow-list in config/cache.php permits the classes Pulse round-trips through the cache (stdClass, Illuminate\Support\Collection, Carbon\CarbonImmutable) β€” the Laravel 13 skeleton ships 'serializable_classes' => false, which makes every Pulse card fail in production with "tried to access a property on an incomplete object" (adds the missing classes when the value is false or an incomplete array)
  • πŸ”§ usesLaravelTelescope() - Validates Laravel Telescope request debugging (partial: adds post-update script and TELESCOPE_ENABLED=false to phpunit.xml if package installed)
  • usesSpatieHealthSetup() - Validates Spatie Health packages, schedules, s3_health disk, and result store configuration
  • πŸ”§ usesSpatieHealthHasCoreChecks() - Validates core health checks (CacheCheck, CpuLoadCheck, DatabaseCheck, DatabaseConnectionCountCheck, DebugModeCheck, EnvironmentCheck, HorizonCheck, QueueCheck, RedisCheck, ScheduleCheck, UsedDiskSpaceCheck) are registered (adds missing checks to Health::checks() in AppServiceProvider)
  • πŸ”§ usesSpatieHealthHasLaravelVersionCheck() - Validates LaravelVersionCheck is registered in Health::checks() (adds to AppServiceProvider)
  • πŸ”§ usesSpatieHealthHasPhpVersionCheck() - Validates PhpVersionCheck is registered in Health::checks() (adds to AppServiceProvider)
  • πŸ”§ usesSpatieHealthHasReleaseAgeCheck() - Validates ReleaseAgeCheck is registered in Health::checks() (adds to AppServiceProvider)
  • usesSpatieHealthCacheCheckCacheStore() - Validates CacheCheck uses the dedicated 'health-checks' cache store via ->driver('health-checks') in AppServiceProvider and config/cache.php
  • usesSpatieHealthQueueCheckCacheStore() - Validates QueueCheck: DispatchQueueCheckJobsCommand is scheduled everyMinute(), uses the dedicated 'health-checks' file cache store in AppServiceProvider and config/cache.php
  • usesSpatieHealthQueueCheckHorizonQueues() - Validates QueueCheck registers all queues from config/horizon.php via onQueue() (requires laravel/horizon)
  • usesSpatieHealthScheduleCheckCacheStore() - Validates ScheduleCheck uses the dedicated 'health-checks' cache store in both AppServiceProvider and config/cache.php
  • πŸ”§ usesSpatieHealthScheduleCheckHeartbeat() - Validates ScheduleCheck is configured with heartbeatMaxAgeInMinutes(2) to prevent false positives (appends the call to ScheduleCheck in AppServiceProvider)
  • usesSpatieBackup() - Validates Spatie Backup database backups with comprehensive config validation

Infrastructure & Dependencies

  • usesPredis() - Validates Predis Redis client is installed
  • isLaravelVersionMaintained() - Validates Laravel 11+ is used
  • πŸ”§ doesNotUseSail() - Validates Sail is NOT used (partial: deletes docker-compose.yml; run composer remove laravel/sail manually)
  • πŸ”§ doesNotUsePhpCsFixer() - Validates PHP CS Fixer is NOT used directly β€” Pint is the formatter (removes the friendsofphp/php-cs-fixer composer.json entry, php-cs-fixer ci-lint entries and .php-cs-fixer.cache; removes .php-cs-fixer.php / .php-cs-fixer.dist.php only once a pint.json exists, so hand-tuned rules are never lost; run composer update afterward)
  • πŸ”§ doesNotUsePhpInsights() - Validates PHP Insights is NOT used (removes the nunomaduro/phpinsights composer.json entry, leftover ci-lint script entries, and config/insights.php; run composer update afterward to sync composer.lock)
  • doesNotUseSpatiePasskeysWithFortify() - Fails if both spatie/laravel-passkeys and laravel/fortify are installed, as they overlap in authentication responsibility
  • doesNotUseBothBaselineRunners() - Fails when package.json also declares @limenet-ch/baseline: the npm runner is the fallback for projects this package cannot reach, and in a Laravel project this one wins (reports the npm uninstall to run; never uninstalls for you)
  • doesNotUseHorizonWatcher() - Validates Spatie Horizon Watcher is NOT installed
  • πŸ”§ doesNotUseGreaterThanOrEqualConstraints() - Validates no >= version constraints in composer.json (use ^ or ~ instead) (replaces >=X.Y with ^X.Y in composer.json)

CI/CD & Deployment

  • hasCiJobs() - Validates GitLab CI pipeline jobs are properly configured (the test job may extend either .test or .test_db)
  • πŸ”§ hasTrivyConfig() - Validates Trivy security scanning CI job, trivy.yaml (scanners, skip-files, skip-dirs, ignorefile, cache.dir, telemetry/VEX/dependency-tree flags, and pkg.include-dev-deps so development dependencies are reported instead of silently skipped), presence of .trivyignore.yaml, and .trivycache/ in .gitignore (creates/merges trivy.yaml, creates an empty .trivyignore.yaml, appends to .gitignore, and adds CI job)
  • callsSentryHook() - Warns if Sentry error tracking is missing (optional)
  • phpVersionMatchesCi() - Validates PHP version consistency with CI configuration
  • isCiLintComplete() - Validates complete linting pipeline
  • πŸ”§ doesNotUseRectorSetProviders() - Fails while rector.php still passes LaravelSetProvider to withSetProviders(): driftingly/rector-laravel 2.6.0 deleted the class, so Rector aborts before doing any work, and withComposerBased(laravel: true) already loads the rules it used to provide (removes the argument, and the call once nothing is left in it; the orphaned import is left to Pint/Rector)
  • πŸ”§ doesNotDuplicateRectorSetRules() - Fails while rector.php lists AddGenericReturnTypeToRelationsRector in withRules(): it already ships in LaravelSetList::LARAVEL_TYPE_DECLARATIONS, which hasRectorConfigWithSets() mandates, and Rector 2.6 warns about the duplicate (removes the entry; the orphaned import is left to Pint/Rector)
  • doesNotUseIgnition() - Validates Ignition debugger is NOT installed

Local Development

  • phpVersionMatchesDdev() - Validates PHP version consistency with DDEV
  • πŸ”§ nodeVersion() - Validates the project pins Node >= 24 (the current LTS) in both package.json engines.node and .nvmrc, compatible with each other (creates the missing constraint β€” establishing Node 24 when none is declared β€” and bumps a declaration that allows anything older to 24; a newer line such as Node 26 is left alone, and a conflict between existing engines.node and .nvmrc is reported, not auto-resolved)
  • πŸ”§ hardensNpmSupplyChain() - Hardens npm against supply-chain attacks: requires package.json engines.npm >= 12 (npm 12 blocks dependency lifecycle scripts by default and refuses git/remote deps), .npmrc engine-strict=true so that requirement is enforced rather than advisory, and .npmrc min-release-age=7 for a 7-day install cooldown that skips freshly-published (potentially compromised) versions (sets engines.npm to ^12, and upserts both .npmrc keys while preserving existing lines)
  • πŸ”§ ddevHasPcovPackage() - Validates DDEV coverage configuration (adds pcov to webimage_extra_packages and creates .ddev/php/90-custom.ini)
  • ddevHasRedisAddon() - Validates DDEV Redis addon is installed and at minimum version 2.2.0
  • πŸ”§ ddevMutagenIgnoresNodeModules() - Validates DDEV Mutagen sync configuration (creates mutagen.yml and fixes .gitignore)
  • πŸ”§ ddevNodeVersionIsAuto() - Validates .ddev/config.yaml sets nodejs_version: auto so DDEV derives the Node version from the project's .nvmrc instead of pinning its own (sets nodejs_version: auto, preserving surrounding comments and formatting)
  • updatesDdevAddons() - Fails if any installed DDEV add-on (.ddev/addon-metadata/*/manifest.yaml) has an install_date older than 3 months; comment shows the ddev add-on get <repository> command to refresh each stale add-on

Build & Release

  • πŸ”§ bumpsComposer() - Validates automatic composer dependency bumping (adds composer bump to post-update-cmd)
  • πŸ”§ releaseItBumpsWordpressThemeVersion() - Validates .release-it.json has an after:bump hook that rewrites the Version: header of style.css, so a release bumps the theme along with composer.json (wordpress profile, themes only; adds a node -e one-liner from policy/policy.json to the hooks)
  • πŸ”§ usesReleaseIt() - Validates automated release management (partial: creates/fixes .release-it.json and adds release npm script if packages installed)
  • πŸ”§ wordpressThemeVersionMatchesComposer() - Validates the Version: header in style.css matches composer.json's version, which @release-it/bumper keeps current (wordpress profile, themes only; rewrites the header from composer.json, or seeds composer.json's version from the theme when it has none)
  • hasNpmScripts() - Validates required npm build scripts
  • πŸ”§ biomeIgnoresCiArtifacts() - Validates that biome.json, when the project has one, keeps Biome away from the files a CI runner leaves in the project root β€” metadata.json, which the GitLab runner extracts with the cache key whenever it restores a cache. Evaluates files.includes the way Biome does (in order, last match wins, no includes meaning every file), so an allowlist that never reaches the root passes. (appends "!metadata.json" to the end of files.includes as a targeted text edit; a config without files.includes is only reported)
  • πŸ”§ biomeIgnoresLaravelLangFiles() - Validates that biome.json, when the project has one, keeps Biome away from Laravel's JSON translation files (lang/*.json, resources/lang/*.json), which translation tooling maintains and Biome's formatter would otherwise churn. Evaluates files.includes like biomeIgnoresCiArtifacts(), probing each pattern with en and with every lang file the project has, so excluding only some locales still fails. (appends "!lang/*.json" / "!resources/lang/*.json" to the end of files.includes as a targeted text edit; a config without files.includes is only reported)
  • πŸ”§ biomeUsesLocalSchema() - Validates that biome.json, when the project has one, points $schema at ./node_modules/@biomejs/biome/configuration_schema.json rather than a version-pinned remote URL, so the schema follows the installed Biome instead of needing a manual bump on every update. Passes when the project does not use Biome. (rewrites or inserts the $schema line as a targeted text edit, leaving the rest of the file β€” comments included β€” byte-identical, since Biome formats biome.json itself)

Security & Configuration

  • πŸ”§ modelShouldBeStrict() - Validates Model::shouldBeStrict() is called in AppServiceProvider with true, no argument, or a dynamic expression (not false) (adds Model::shouldBeStrict(! app()->isProduction()) to boot())
  • πŸ”§ formRequestFailOnUnknownFields() - Validates FormRequest::failOnUnknownFields() is called in AppServiceProvider (Laravel β‰₯13.6 only; warns on older versions) (adds FormRequest::failOnUnknownFields(! app()->isProduction()) to boot())
  • hasDailyLogging() - Validates logging uses daily channel (directly or via stack)
  • hasEncryptedEnvFile() - Validates encrypted environment file exists
  • usesReadableEncryptedEnvFile() - Validates the encrypted env file uses the readable line-per-variable format produced by ddev artisan env:encrypt --readable (variable names stay visible in diffs), not the opaque blob format. Passes when no encrypted file exists (existence is hasEncryptedEnvFile's concern).
  • doesNotHaveStrayEnvFiles() - Fails if a plaintext .env* file other than .env, .env.example, .env.testing, .env.vite or an encrypted .env.*.encrypted sits in the project root (allowlist in policy/policy.json). A gitignored .env.production left behind by env:decrypt is an unreviewed second copy of production secrets; re-encrypt any changes with ddev artisan env:encrypt --readable, then delete it.
  • doesNotPinOldMailTemplate() - Fails if a published mail view that pins the old template (resources/views/vendor/mail/html/themes/default.css or html/header.blade.php) exists, preventing adoption of Laravel's modernized mail template.
  • πŸ”§ callsBaseline() - Validates self-validation runs after updates (adds/upgrades post-update-cmd entry to include --fix)
  • πŸ”§ doesNotExcludeUnknownChecks() - Fails when config/baseline.php excludes a name no registered check answers to: excludes are matched against the registry, so an entry left behind by a check this package renamed or removed silences nothing and only hides that the exclusion is no longer in force (drops the dead entries, leaving the remaining excludes and the periodic state intact)
  • doesNotCallPeriodicBaselineOnUpdate() - Fails if php artisan limenet:laravel-baseline:periodic is in the post-update-cmd scripts (it shouldn't be β€” periodic checks fail CI automatically when expired)
  • πŸ”§ doesNotHaveGuidelinesScript() - Fails if the removed php artisan limenet:laravel-baseline:guidelines command is still in post-update-cmd (removed in v2.1.0) (removes the entry from composer.json)
  • πŸ”§ isInstalledAsRegularDependency() - Validates limenet/laravel-baseline is in require (not require-dev), since a Laravel app loads it at runtime (moves from require-dev to require in composer.json)
  • πŸ”§ isInstalledAsDevDependency() - Validates limenet/laravel-baseline is in require-dev (not require): outside Laravel it is only a development tool, so a --no-dev deploy leaves it out (php and wordpress profiles; moves from require to require-dev in composer.json)
  • πŸ”§ usesLaravelLang() - Validates laravel-lang/lang dev dependency is installed with lang:update and pint in post-update scripts (partial: adds post-update scripts if package in require-dev)

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.