A Composer plugin that applies patches to Magento 2 and Adobe Commerce, re-checks them against the files on disk on every Composer run, and fails the build when one is missing. I built it in September 2026, when m2-meta-security-patches outgrew vaimo/composer-patches. The meta-package has applied its patches through it since 2026.09.11-beta2.
Why it replaces vaimo/composer-patches
vaimo/composer-patches failed in three ways on real stores, and each one left a store unpatched while reporting it as fine:
- It trusts its own record. vaimo decides a patch is applied from a note of what it applied last time, never from the file. After
composer reinstall magento/magento2-baseput an unpatchedlib/web/underscore.jsback, it still said[APPLIED](vaimo#162, open as of 11th October 2026). Here every verdict comes from asking git about the file on disk, so a reverted patch is found and applied again. - One replaced package drops the whole patch. When a module replaces a package that one file of a patch belongs to, vaimo skips the entire patch without a word. A store running
outeredge/magento-disable-graphqllost a July isolated patch that way (m2-meta-security-patches#7). Here a patch is split per file: the missing file is named and skipped, and the rest applies. - Any dependency can patch your code. Every installed package may declare patches against the project by default (vaimo#157, open and unanswered). Here only packages named in
extra.magento-patches.trustcan.
The switch from vaimo covers each of these in full, and the upgrade for meta-package users.
What a missing patch looks like
The screenshot above is composer magento-patches:verify on a Magento 2.4.8-p5 store running m2-meta-security-patches 2026.10.09. I reverted one of the two files in the Polyshell patch (APSB25-94) with git apply -R. Before that it printed 61 in place and exited 0. After, it names the patch and the file, and exits 1.
The exit code is what a pipeline acts on:
0: every patch that applies to this store is in place.1: a patch applies but is not in place. The silent-revert case.2: conflict. A file matches neither the patched nor the unpatched side.3: configuration or trust error, such as a package that ships patches but is missing fromtrust.
All four commands (list, status, apply and verify) take --json for pipelines that would rather parse the result than read it.
Installing
Allow the plugin first, then require it, then name the packages allowed to ship patches:
composer config --no-plugins allow-plugins.samjuk/magento-patch-installer true
composer require samjuk/magento-patch-installer
composer config --json extra.magento-patches.trust '["samjuk/*"]'
The order matters. In CI, Composer skips a plugin it has not been told to trust, says nothing, and exits 0 having applied no patches. And nothing outside your own composer.json can patch the project until it is named in trust. A package that declares patches without being trusted fails the run and is named, rather than quietly skipped.
It needs Magento 2.4.2 or later, PHP 7.4+, Composer 2.2+ and git on the path.
How it decides a patch is applied
By asking git about the file on disk, every time. Each patch is split into one fragment per target file, and each fragment gets two questions:
git apply -R --check | git apply --check | Verdict |
|---|---|---|
| passes | Already applied, skip | |
| fails | passes | Applicable, apply it |
| fails | fails, file absent | Not applicable, say why |
| fails | fails, file present | Conflict, reported and never overwritten |
The reverse check goes first and wins, because a hunk with boilerplate context can match at more than one offset, and applying again would duplicate the change. An exit code is not taken as proof of a write either: after applying, the file’s hash has to have moved.
A patch applies as a unit. If one of its files refuses it, none are written, because a security fix on three of its four files can break the site. A file the store does not have, like the replaced GraphQL module above, is named and skipped while the rest applies.
It runs at priority -1000 after composer install and composer update, so after magento-composer-installer has deployed the root files, and again on composer dump-autoload. Root-mapped files such as lib/web/underscore.js are patched in both the served copy and the package copy. var/composer-patches/state.json records what was written, but only as an audit log: deleting it changes no verdict.
Using it as a deploy gate
Run verify as its own step after the install:
- run: composer install --no-interaction
- run: composer magento-patches:verify --no-interaction
composer install --no-plugins and a missing allow-plugins entry both finish green over an unpatched store, so the gate has to sit outside the plugin. It also catches what Composer never sees. An rsync, a cp -r in a Dockerfile or setup:upgrade re-deploying magento2-base fires no Composer event, but verify reads the working tree.
Alongside vaimo and cweagans
It installs next to vaimo/composer-patches or cweagans/composer-patches, with its own config key and magento-patches:* commands, so a store can keep either for its own patches. The README records testing it next to both on real 2.4.6-p15 and 2.4.8-p5 installs, including a live store with 18 packages already patched by cweagans. Patches another tool applied are recognised as applied, so moving over does not mean applying everything again.
What it does not do
- It does not fetch patches from URLs. Every patch ships inside a package, and so does the end-of-life data it warns about.
- It does not overwrite a conflict. The file is reported and left for a person to decide.
- It does not let a patch be switched off quietly. A skip needs a written reason in the root
composer.json, and it shows in every report.
Testing and releases
CI lints and runs the unit tests on PHP 7.4 to 8.5, runs PHPStan at level 8, and runs acceptance tests against real Magento 2.4.3, 2.4.6-p15, 2.4.7-p10, 2.4.8-p5 and 2.4.9 installs in the Magento CI testing images. Releases run from v0.1.0 (10th September 2026) to v0.2.1 (9th October 2026); check GitHub releases for the latest.