diff --git a/composer.json b/composer.json
index 44b4c9bc36baf6a25f81fc55544accc4beca213a..0b52046f036cf8019ae281578600dec794db09e3 100644
--- a/composer.json
+++ b/composer.json
@@ -6,6 +6,7 @@
     "require": {
         "composer/installers": "^1.0.24",
         "drupal/core": "self.version",
+        "drupal/core-vendor-hardening": "self.version",
         "wikimedia/composer-merge-plugin": "^1.4"
     },
     "require-dev": {
@@ -72,9 +73,6 @@
         "pre-install-cmd": "Drupal\\Core\\Composer\\Composer::ensureComposerVersion",
         "pre-update-cmd": "Drupal\\Core\\Composer\\Composer::ensureComposerVersion",
         "pre-autoload-dump": "Drupal\\Core\\Composer\\Composer::preAutoloadDump",
-        "post-autoload-dump": "Drupal\\Core\\Composer\\Composer::ensureHtaccess",
-        "post-package-install": "Drupal\\Core\\Composer\\Composer::vendorTestCodeCleanup",
-        "post-package-update": "Drupal\\Core\\Composer\\Composer::vendorTestCodeCleanup",
         "phpcs": "phpcs --standard=core/phpcs.xml.dist --runtime-set installed_paths $($COMPOSER_BINARY config vendor-dir)/drupal/coder/coder_sniffer --",
         "phpcbf": "phpcbf --standard=core/phpcs.xml.dist --runtime-set installed_paths $($COMPOSER_BINARY config vendor-dir)/drupal/coder/coder_sniffer --"
     },
@@ -86,6 +84,10 @@
         {
             "type": "path",
             "url": "core"
+        },
+        {
+            "type": "path",
+            "url": "composer/Plugin/VendorHardening"
         }
     ]
 }
diff --git a/composer.lock b/composer.lock
index 1ae9956e4ba28db46fd1a22a5e8b56e2c0648e6a..52aa037f7b4bcf5e8b233278651097e8d9a68ad1 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "bc036f74a7ef48097e813b50a2d5aea8",
+    "content-hash": "63b940ec40ef24930a101dfce6eed82a",
     "packages": [
         {
             "name": "asm89/stack-cors",
@@ -911,6 +911,36 @@
             ],
             "description": "Drupal is an open source content management platform powering millions of websites and applications."
         },
+        {
+            "name": "drupal/core-vendor-hardening",
+            "version": "9.0.x-dev",
+            "dist": {
+                "type": "path",
+                "url": "composer/Plugin/VendorHardening",
+                "reference": "2db54f089065dedbe4a040b01f7b527f2bad68f6"
+            },
+            "require": {
+                "composer-plugin-api": "^1.1",
+                "php": ">=7.0.8"
+            },
+            "type": "composer-plugin",
+            "extra": {
+                "class": "Drupal\\Composer\\Plugin\\VendorHardening\\VendorHardeningPlugin"
+            },
+            "autoload": {
+                "psr-4": {
+                    "Drupal\\Composer\\Plugin\\VendorHardening\\": "."
+                }
+            },
+            "license": [
+                "GPL-2.0-or-later"
+            ],
+            "description": "Hardens the vendor directory for when it's in the docroot.",
+            "homepage": "https://www.drupal.org/project/drupal",
+            "keywords": [
+                "drupal"
+            ]
+        },
         {
             "name": "easyrdf/easyrdf",
             "version": "0.9.1",
@@ -6229,6 +6259,7 @@
     "minimum-stability": "dev",
     "stability-flags": {
         "drupal/core": 20,
+        "drupal/core-vendor-hardening": 20,
         "behat/mink": 20,
         "behat/mink-selenium2-driver": 20
     },
diff --git a/composer/Plugin/VendorHardening/VendorHardeningPlugin.php b/composer/Plugin/VendorHardening/VendorHardeningPlugin.php
index 7e36e0e9e18440ea6dd35bdd07fb91e198fcc2ce..71410c4dd6dd4d4b37fc35a5be749490064cd426 100644
--- a/composer/Plugin/VendorHardening/VendorHardeningPlugin.php
+++ b/composer/Plugin/VendorHardening/VendorHardeningPlugin.php
@@ -5,12 +5,13 @@
 use Composer\Composer;
 use Composer\EventDispatcher\EventSubscriberInterface;
 use Composer\Installer\PackageEvent;
+use Composer\Installer\PackageEvents;
 use Composer\IO\IOInterface;
+use Composer\Package\CompletePackage;
 use Composer\Plugin\PluginInterface;
+use Composer\Script\Event;
 use Composer\Script\ScriptEvents;
 use Composer\Util\Filesystem;
-use Composer\Script\Event;
-use Composer\Installer\PackageEvents;
 
 /**
  * A Composer plugin to clean out your project's vendor directory.
@@ -70,6 +71,8 @@ public static function getSubscribedEvents() {
       ScriptEvents::POST_AUTOLOAD_DUMP => 'onPostAutoloadDump',
       ScriptEvents::POST_UPDATE_CMD => 'onPostCmd',
       ScriptEvents::POST_INSTALL_CMD => 'onPostCmd',
+      PackageEvents::PRE_PACKAGE_INSTALL => 'onPrePackageInstall',
+      PackageEvents::PRE_PACKAGE_UPDATE => 'onPrePackageUpdate',
       PackageEvents::POST_PACKAGE_INSTALL => 'onPostPackageInstall',
       PackageEvents::POST_PACKAGE_UPDATE => 'onPostPackageUpdate',
     ];
@@ -95,6 +98,28 @@ public function onPostCmd(Event $event) {
     $this->cleanAllPackages($this->composer->getConfig()->get('vendor-dir'));
   }
 
+  /**
+   * PRE_PACKAGE_INSTALL event handler.
+   *
+   * @param \Composer\Installer\PackageEvent $event
+   */
+  public function onPrePackageInstall(PackageEvent $event) {
+    /** @var \Composer\Package\CompletePackage $package */
+    $package = $event->getOperation()->getPackage();
+    $this->removeBinBeforeCleanup($package);
+  }
+
+  /**
+   * PRE_PACKAGE_UPDATE event handler.
+   *
+   * @param \Composer\Installer\PackageEvent $event
+   */
+  public function onPrePackageUpdate(PackageEvent $event) {
+    /** @var \Composer\Package\CompletePackage $package */
+    $package = $event->getOperation()->getTargetPackage();
+    $this->removeBinBeforeCleanup($package);
+  }
+
   /**
    * POST_PACKAGE_INSTALL event handler.
    *
@@ -119,6 +144,84 @@ public function onPostPackageUpdate(PackageEvent $event) {
     $this->cleanPackage($this->composer->getConfig()->get('vendor-dir'), $package_name);
   }
 
+  /**
+   * Remove bin config for packages that would have the bin file removed.
+   *
+   * Where the configured bin files are in the directories to be removed, remove
+   * the bin config.
+   *
+   * @param \Composer\Package\CompletePackage $package
+   *   The package we're cleaning up.
+   */
+  protected function removeBinBeforeCleanup(CompletePackage $package) {
+    // Only do this if there are binaries and cleanup paths.
+    $binaries = $package->getBinaries();
+    $clean_paths = $this->config->getPathsForPackage($package->getName());
+    if (!$binaries || !$clean_paths) {
+      return;
+    }
+    if ($unset_these_binaries = $this->findBinOverlap($binaries, $clean_paths)) {
+      $this->io->writeError(
+        sprintf('%sModifying bin config for <info>%s</info> which overlaps with cleanup directories.', str_repeat(' ', 4), $package->getName()),
+        TRUE,
+        IOInterface::VERBOSE
+      );
+      $modified_binaries = [];
+      foreach ($binaries as $binary) {
+        if (!in_array($binary, $unset_these_binaries)) {
+          $modified_binaries[] = $binary;
+        }
+      }
+      $package->setBinaries($modified_binaries);
+    }
+  }
+
+  /**
+   * Find bin files which are inside cleanup directories.
+   *
+   * @param string[] $binaries
+   *   'Bin' configuration from the package we're cleaning up.
+   * @param string[] $clean_paths
+   *   The paths we're cleaning up.
+   *
+   * @return string[]
+   *   Bin files to remove, with the file as both the key and the value.
+   */
+  protected function findBinOverlap($binaries, $clean_paths) {
+    // Make a filesystem model to explore. This is a keyed array that looks like
+    // all the places that will be removed by cleanup. 'tests/src' becomes
+    // $filesystem['tests']['src'] = TRUE;
+    $filesystem = [];
+    foreach ($clean_paths as $clean_path) {
+      $clean_pieces = explode("/", $clean_path);
+      $current = &$filesystem;
+      foreach ($clean_pieces as $clean_piece) {
+        $current = &$current[$clean_piece];
+      }
+      $current = TRUE;
+    }
+    // Explore the filesystem with our bin config.
+    $unset_these_binaries = [];
+    foreach ($binaries as $binary) {
+      $binary_pieces = explode('/', $binary);
+      $current = &$filesystem;
+      foreach ($binary_pieces as $binary_piece) {
+        if (!isset($current[$binary_piece])) {
+          break;
+        }
+        else {
+          // Value of TRUE means we're at the end of the path.
+          if ($current[$binary_piece] === TRUE) {
+            $unset_these_binaries[$binary] = $binary;
+            break;
+          }
+        }
+        $current = &$filesystem[$binary_piece];
+      }
+    }
+    return $unset_these_binaries;
+  }
+
   /**
    * Gets a list of all installed packages from Composer.
    *
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/VendorHardening/VendorHardeningPluginTest.php b/core/tests/Drupal/Tests/Composer/Plugin/VendorHardening/VendorHardeningPluginTest.php
index cb894bc5741e06146ce8a99e73dd0d831b182e35..f98dd25b2d975fae1e2821bf4fc5a29963f2c2fa 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/VendorHardening/VendorHardeningPluginTest.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/VendorHardening/VendorHardeningPluginTest.php
@@ -122,6 +122,9 @@ public function testCleanAllPackages() {
     $this->assertFileNotExists(vfsStream::url('vendor/drupal/package/tests'));
   }
 
+  /**
+   * @covers ::writeAccessRestrictionFiles
+   */
   public function testWriteAccessRestrictionFiles() {
     $dir = vfsStream::url('vendor');
 
@@ -148,4 +151,64 @@ public function testWriteAccessRestrictionFiles() {
     $this->assertFileExists($dir . '/web.config');
   }
 
+  public function providerFindBinOverlap() {
+    return [
+      [
+        [],
+        ['bin/script'],
+        ['tests'],
+      ],
+      [
+        ['bin/composer' => 'bin/composer'],
+        ['bin/composer'],
+        ['bin', 'tests'],
+      ],
+      [
+        ['bin/composer' => 'bin/composer'],
+        ['bin/composer'],
+        ['bin/composer'],
+      ],
+      [
+        [],
+        ['bin/composer'],
+        ['bin/something_else'],
+      ],
+      [
+        [],
+        ['test/script'],
+        ['test/longer'],
+      ],
+      [
+        ['bin/very/long/path/script' => 'bin/very/long/path/script'],
+        ['bin/very/long/path/script'],
+        ['bin'],
+      ],
+      [
+        ['bin/bin/bin' => 'bin/bin/bin'],
+        ['bin/bin/bin'],
+        ['bin/bin'],
+      ],
+      [
+        [],
+        ['bin/bin'],
+        ['bin/bin/bin'],
+      ],
+    ];
+  }
+
+  /**
+   * @covers ::findBinOverlap
+   * @dataProvider providerFindBinOverlap
+   */
+  public function testFindBinOverlap($expected, $binaries, $clean_paths) {
+    $plugin = $this->getMockBuilder(VendorHardeningPlugin::class)
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $ref_find_bin_overlap = new \ReflectionMethod($plugin, 'findBinOverlap');
+    $ref_find_bin_overlap->setAccessible(TRUE);
+
+    $this->assertSame($expected, $ref_find_bin_overlap->invokeArgs($plugin, [$binaries, $clean_paths]));
+  }
+
 }