Restricting vaimo/composer-patches to Trusted Packages
vaimo/composer-patches applies patches declared under extra.patches in any installed package’s composer.json, not just the root project. By default, every dependency in your tree is a potential patch source — a compromised or malicious package can silently patch your codebase on composer install/update with no review step.
The fix
Restrict patch sources to a whitelist of trusted packages:
composer config --json "extra.patcher" '{"sources":{"packages":["your-vendor/your-trusted-package"]}}'
This restricts patches to only the listed package(s) plus the root project itself (sources.project stays implicitly enabled). Every other dependency — however deep in the tree — is excluded as a patch source.
Gotcha 1: composer config silently writes the wrong shape past two levels
composer config --json only nests two levels deep under extra. Anything beyond that gets written as a literal dotted key instead of nested JSON:
composer config --json "extra.patcher.sources.packages" '["your-vendor/your-trusted-package"]'
produces:
{
"extra": {
"patcher": {
"sources.packages": ["your-vendor/your-trusted-package"]
}
}
}
Note "sources.packages" as a single flat key — not "sources": {"packages": [...]}. The plugin reads extra.patcher.sources.packages as real nested config, so this does nothing. No error, no warning — your project just stays unrestricted. Always pass the whole extra.patcher value as one JSON object (as in the fix above) rather than a deep dotted path.
Gotcha 2: composer patch:list can’t validate the restriction
patch:list forces all sources on internally for display purposes, regardless of what’s in extra.patcher.sources. Seeing a patch listed there is not proof it will actually apply — it’s not proof it’s blocked either. Don’t use it to sanity-check this config.
To actually verify which packages are eligible patch sources, resolve it directly:
php -r '
require "vendor/autoload.php";
$io = new Composer\IO\NullIO();
$composer = Composer\Factory::create($io, "composer.json");
$factory = new Vaimo\ComposerPatches\Factories\ConfigFactory(
(new Vaimo\ComposerPatches\Factories\ComposerContextFactory($composer))->create()
);
$pluginConfig = $factory->create();
$sourcesFactory = new Vaimo\ComposerPatches\Factories\SourcesResolverFactory($composer);
$sourcesResolver = $sourcesFactory->create($pluginConfig);
$repo = $composer->getRepositoryManager()->getLocalRepository();
foreach ($sourcesResolver->resolvePackages($repo) as $p) {
echo $p->getName() . PHP_EOL;
}
'
Run it before and after the config change — the package list should shrink to just the root project and your whitelisted package(s).
Gotcha 3: an empty packages whitelist doesn’t mean what it looks like
You’d expect "packages": [] to mean “no packages allowed”. Read in isolation, PackageSource actually treats an empty array as “no filter — allow everything” (empty($this->packages) short-circuits to returning all packages). It only behaves as “block all” because of how the plugin merges config upstream: an empty packages array is falsy, so it gets dropped entirely by array_filter() before the packages and vendors sources are even constructed — leaving only the project source active.
That chain is easy to break with future plugin versions or unrelated config changes. Don’t rely on an empty array as your “block everything” state in committed config — always land on an explicit non-empty whitelist, and use the verification script above (not patch:list) to confirm the empty-array state actually blocked everything if you’re testing the mechanism itself.