kalessil/production-dependencies-guard
A Composer plugin that guards production installs by detecting dev-only dependencies and blocking them from leaking into production builds. It adds a safety check so accidental require-dev packages or non-production installs are caught early.
kalessil/production-dependencies-guard is a Composer plugin, not a Laravel package — it has no service provider, config file, or artisan command. It hooks Composer's install/update/require events and fails (or warns) when a package that looks like a development tool is placed in require instead of require-dev. That is its entire job.
Minimal setup in a Laravel app:
composer require --dev kalessil/production-dependencies-guard
Then, because Composer 2.2+ blocks plugins by default, allow it in composer.json:
"config": {
"allow-plugins": {
"kalessil/production-dependencies-guard": true
}
}
Run composer dump-autoload (or any install/update) and confirm it loads without a "plugin was not loaded" error.
Where to look first:
composer.json — check "type": "composer-plugin", the extra.class entry, and which Composer events it subscribes to. That tells you precisely when it runs.composer.json require vs require-dev diff — that is what the guard evaluates.First use case (the one that matters in Laravel): the usual suspects get composer require'd by mistake — barryvdh/laravel-debugbar, laravel/telescope, laravel/sail, barryvdh/laravel-ide-helper, nunomaduro/collision, larastan/larastan, phpunit/phpunit, pestphp/pest. Run:
composer require barryvdh/laravel-debugbar # no --dev
and watch it get flagged. The correct form is always:
composer require --dev barryvdh/laravel-debugbar
1. Global install for team-wide enforcement. Install once into your global Composer home so it protects every project without touching each composer.json:
composer global require kalessil/production-dependencies-guard
Note the same allow-plugins rule applies to the global config. This is the lowest-friction rollout for a team, but it is invisible to new hires and CI, so pair it with a per-project declaration.
2. Per-project require-dev + CI gate. Commit it as a dev dependency and let composer install in CI run the plugin. Because the plugin aborts the command on violation, a PR that adds a dev tool to require fails the build before merge:
# .github/workflows/ci.yml
- run: composer install --no-interaction --prefer-dist
This is the pattern with the highest long-term value — the guard becomes a policy check, not a personal habit.
3. Composer script alias. Wrap it so developers can run it deliberately, not only when Composer happens to fire:
"scripts": {
"guard:deps": [
"@composer validate --strict",
"@composer install --dry-run"
]
}
Exact event names vary — verify against the plugin's README, since some Composer plugins expose a dedicated command while others only react to install/update events.
4. Remediation workflow when the guard fires. Composer does not move packages between sections for you:
composer remove barryvdh/laravel-debugbar
composer require --dev barryvdh/laravel-debugbar
Do this in one commit so composer.json and composer.lock stay consistent.
5. Layer with complementary Composer tooling. The guard answers "is this in the right section?" It does not answer "is this package actually used?" or "is this version vulnerable?" Pair it with composer-unused, composer-require-checker, and roave/security-advisories. Together they cover section hygiene, dead dependencies, and CVEs.
6. Production deploy scripts. Keep composer install --no-dev --optimize-autoloader in your deploy pipeline. The guard lives in require-dev, so it is intentionally absent in production — it is a development-time policy check, not a runtime protection.
allow-plugins is the number-one blocker. On Composer 2.2+ you will get a plugin-blocked error and assume the package is broken. It isn't. Add the explicit entry or "allow-plugins": true (the latter is discouraged).--no-plugins and --no-scripts silently disable it. Docker builds, some CI images, and deploy scripts often pass these flags. A "passing" build may simply mean the guard never ran.symfony/var-dumper belongs in require-dev in a library but is defensible in require for a Laravel app that calls dd() in production support tooling. The guard doesn't know your intent; you do."type": "path", the guard has less metadata to reason about and may miss violations entirely, or flag an internal package oddly. Check the README for an ignore/exclusion list before fighting it.config/production-dependencies-guard.php or php artisan commands — there are none. If you need configurable rules, you're forking it or writing your own check.-vvv to see the plugin's event output inline. Because it runs mid-Composer, the message is interleaved with dependency resolution output and is easy to miss in noisy CI logs.--no-interaction before trusting it in a pipeline.require-dev, and accept that if it breaks against a future Composer major you may need to drop it. The fallback is ~20 lines of your own: a PHP script that reads composer.json, cross-references packages found only in require-dev of your own history, and exits non-zero. Keep that escape hatch in mind before making it load-bearing in CI.composer validate --strict; a malformed composer.json produces confusing plugin errors that mask the real syntax problem.How can I help you explore Laravel packages today?