drupal/core-vendor-hardening
Composer plugin for Drupal that hardens the vendor directory by stripping unnecessary files such as tests, documentation, and version-control metadata from dependencies. It reduces deployment size and attack surface while keeping required code intact.
drupal/core-vendor-hardening is a Composer plugin that strips development files (tests, docs, .git, etc.) out of vendor/ packages and drops an .htaccess/web.config deny file into the vendor directory — but only meaningfully when vendor/ is web-accessible.
Install it:
composer require drupal/core-vendor-hardening
Put it in require (not require-dev) if production installs must harden; a --no-dev build will not install a dev-only plugin and therefore will not run it.
Allow the plugin under Composer 2:
"config": {
"allow-plugins": {
"drupal/core-vendor-hardening": true
}
}
Point it at the web root in the root composer.json:
"extra": {
"drupal-core-vendor-hardening": {
"web-root": "."
}
}
Use "." for a flat layout where the project root is the docroot, or "web" if the docroot is web/ and vendor/ was moved inside it.
Run composer install or composer update and verify the result:
ls -la vendor/.htaccess
ls vendor/some/package
Where to look first: the extra.drupal-core-vendor-hardening block in composer.json, then the plugin source (typically src/VendorHardeningPlugin.php in the package) to see the exact cleanup patterns and event subscriptions. composer install -vvv and COMPOSER_DEBUG_EVENTS=1 show when the plugin fires.
First use case: a legacy flat-docroot deployment where your only web server protection for vendor/ is Apache .htaccess, and you want to shrink the deploy artifact by removing test suites and docs from every package.
Flat-docroot hardening (the intended case). Configure "web-root": "." so the plugin recognizes vendor/ as inside the docroot. On post-install/post-update it writes vendor/.htaccess with Require all denied / Deny from all and prunes configured junk files from each installed package.
Verify in CI. Add a cheap assertion so a future config change doesn't silently turn hardening off:
test -f vendor/.htaccess || { echo "vendor hardening not applied"; exit 1; }
grep -q "Deny from all\|Require all denied" vendor/.htaccess
Deploy pipeline. If the plugin lives in require-dev, build artifacts in a stage that installs dev dependencies, run hardening, then produce the production tree. If it lives in require, a plain composer install --no-dev --optimize-autoloader during deploy is enough — but remember the plugin must actually be installed for it to run.
Pair with drupal/core-composer-scaffold. Scaffold copies files into the web root; vendor hardening prunes the vendor tree. Keep scaffold config explicit and run a full composer install after changing either so both plugins re-execute.
Complement with server config (nginx). The plugin only writes Apache/IIS files. For nginx, add explicitly:
location ^~ /vendor/ { deny all; return 404; }
Customization is limited. The cleanup list is largely baked into the plugin; per-package whitelists are not a first-class option. If one package breaks, the practical paths are (a) patch the plugin with cweagans/composer-patches, (b) fork it, or (c) accept the hardcoded list and document the exception.
vendor/ at project root, docroot web/), the plugin has nothing to protect and effectively does nothing. Confirm the vendor path actually sits under the configured web-root before expecting results.--no-scripts and --no-plugins kill it. --no-plugins is obvious; --no-scripts suppresses the script-event hooks, so the post-command pass won't run. Package-level install events may still fire, which can lead to partially hardened trees — verify after any unusual install invocation.require-dev plus composer install --no-dev in production equals no hardening at deploy time. Pick deliberately and test the exact production command.tests/ or docs/. If a package starts throwing "file not found" after enabling the plugin, diff the vendor tree (git diff --no-index) to see what was pruned..htaccess does nothing on nginx, Caddy, or a CDN origin. Always pair with server-level deny rules.web.config; don't strip it from your deploy artifact.vendor/; ensure the build user owns it, otherwise composer install fails halfway through.web-root won't retroactively harden an existing tree — run composer install (or composer update) again.vendor/ is a symlink; keep it a real directory.vendor/ outside the docroot, skip this package entirely.How can I help you explore Laravel packages today?