lockrot¶
Finds abandoned, unmaintained and branch-pinned packages in composer.lock — and releases that
promise a PHP version they were never tested against.
Install¶
composer require --dev somework/lockrot
composer config allow-plugins.somework/lockrot true
composer lockrot --target-php=8.4
Adds one dev dependency and one command. It also prints a short summary during
composer require, update and install, above the operations list — see
Install-time summary.
Nothing is added to your project. For the checksum-verified download and how
self-update works, see PHAR and self-update.
composer global update keeps it current. Because it is a plugin rather than a
PHAR, the install-time summary then runs in every project you touch;
Install-time summary explains how to turn that off.
Requires PHP 7.4 or newer and Composer 2.2 or newer.
What a run looks like¶
critical (3) abandoned sensio/framework-extra-bundle v6.2.10 direct marked abandoned by its repository, replacement: Symfony; repository archived on GitHub; last release 2023-02-24 (3.6 years ago); last push 2023-02-24 (3.6 years ago); released 2023-02-24, before PHP 8.4 GA (2024-11-21); php constraint ">=7.2.5" has no upper bound; pulls in 1 flagged package: doctrine/annotations (abandoned) silent javibravo/simpleue 2.1.0 direct last release 2017-11-15 (8.8 years ago); last push 2017-11-18 (8.8 years ago); released 2017-11-15, before PHP 8.4 GA (2024-11-21); php constraint ">=5.5" has no upper bound silent mnapoli/piwik-twig-extension 3.0.0 direct last release 2020-04-24 (6.4 years ago); last push 2020-04-28 (6.4 years ago); released 2020-04-24, before PHP 8.4 GA (2024-11-21); php constraint ">=7.0" has no upper bound high (64) abandoned behat/transliterator v1.5.0 via stof/doctrine-extensions-bundle › gedmo/doctrine-extensions marked abandoned by its repository; repository archived on GitHub; last release 2022-03-30 (4.5 years ago); released 2022-03-30, before PHP 8.4 GA (2024-11-21); php constraint ">=7.2" has no upper bound
The first 17 lines of a real run against wallabag's 200-package lock file, at 120 columns. Four of its 83 findings — read the whole report.
A whole run on a smaller project, 156 packages, from the command to the summary block and the footer:

What it looks for¶
abandoned— the package's own repository says so: Packagist carries theabandonedmarker a maintainer set by hand, or the repository is archived on GitHub or GitLab.silent— five years with no stable release and five years with no push to the repository, on the default thresholds. Nobody announced anything; the package simply stopped.pinned— your lock file holds a branch snapshot (dev-master,dev-main) or a commit hash instead of a released version, so the thing you installed has no version number anyone else can ask for.old-promise— the release predates the PHP version you are upgrading to, and itsrequire.phpconstraint is open-ended (>=7.2), so Composer accepted it on a PHP nobody released it against.composer check-platform-reqsis satisfied too:>=7.2is true on 8.4.left-behind— the release branch you installed from has had no release for years while a higher branch of the same package keeps shipping.composer outdated --major-onlysays a newer major exists; this says the branch you are on gets no fixes, and from 0.8.0 on names the constraint that follows the fixes (require ^7.2 to follow), written ascomposer requirewrites it.
A sixth verdict, stale, catches a package that is old on one of those fronts but not both — worth
knowing, rarely worth acting on. Every finding carries the evidence behind it, the date the data was
read, and the chain of requirements that pulled the package in — what composer why shows for one
package, for every finding at once: every direct requirement it is reachable from, not only the
shortest one, and each direct requirement says what it pulls in.
What it reports has all nine verdicts and the priority rules.
"Composer already warns me about abandoned packages"¶
It does: Package X is abandoned, you should avoid using it on every install, and
composer audit --abandoned reports the same packages. Both read one field: the abandoned marker
a maintainer sets by hand on Packagist, as Composer copied it into composer.lock the last time it
resolved the package. Most packages that stop being maintained never get it, because setting it is
the last act of someone who has already walked away — so the field is accurate when it is there,
and silent the rest of the time. Across the lock files of 31 open-source PHP applications,
composer audit --abandoned reported 62 of 129 abandoned or silent packages.
composer outdated answers a different question, whether a newer version exists: a package can be
fully up to date and dead, or two majors behind and fine.
lockrot reads that field too, and then keeps going. It asks when the last stable release actually
landed, when the repository was last pushed to, whether your lock file is holding a branch snapshot
rather than a version, and whether a release made an open-ended PHP promise it was never tested
against. It reads composer.lock and composer.json, and it writes to neither.
The security advisories composer audit reports are on lockrot's findings too, fetched through the
same Composer advisory API. From 0.7.0 on, an advisory on an abandoned, silent or left-behind
package that no listed release fixes reads no fix expected and raises the priority one step: the
vulnerability is the same, the chance of a patch is not. From 0.8.0 on, when the abandoned package's
repository names a replacement, the line says where the fix lands instead — no fix expected;
migrate to symfony/mailer.
In CI¶
Nothing fails a build until you ask it to: fail-on is none by default, exit 1 means a finding
reached the threshold you chose, and exit 2 is reserved for lockrot's own errors. On GitHub
Actions, uses: somework/lockrot-action@v1 is the whole step. Recipes for that, GitLab CI and PR
comments are in In CI; a baseline lets you accept what you have today and
fail only on what arrives tomorrow.
Reading a report someone sent you¶
--format=html writes the whole run as one page that opens from a file and needs no server. When
what you have is the JSON instead, the report viewer draws it the
same way: paste it, or drop either file in. Nothing is uploaded — the rendering happens in your
browser, in a sandbox with no way to reach the network — and it is on a host of its own rather than
this one, because a page that draws documents written by strangers does not belong on the domain
that hands out the archive. Reading a report is what the verdicts and the priorities
in one mean.
Questions¶
Does it change composer.json or composer.lock?¶
No. It reads both and writes to neither. The only file it ever writes is the
baseline, and only when you pass --generate-baseline.
Can it break composer install?¶
Not unless you ask it to. The install-time summary prints at most 10 lines
within a 5-second budget and never stops the transaction; a failed lookup is reported, not raised.
install-time-strict is the opt-in that lets it stop one, "install-time": "off" turns the summary
off for a project, and LOCKROT_DISABLE=1 silences lockrot for one command.
Do I need a GitHub token?¶
No, but it sees more with one. Anonymously GitHub allows 60 requests an hour, so lockrot checks
repository activity only for packages whose releases already look stale, at most 50 per host per
run, and reports how many the cap affected. With GITHUB_TOKEN set, or Composer's own
github-oauth, every package is checked.
Data sources has the GitLab and
Bitbucket rules.
Does it work with Private Packagist, Satis or a mirror?¶
Yes, with nothing to configure. lockrot reads the repositories already in your composer.json
through Composer's own repository layer, with Composer's authentication and proxy settings.
Does it work offline, and what happens when the network fails?¶
--offline serves everything from Composer's cache and lockrot's own. A network failure never
turns into a non-zero exit code on its own: the check that could not run is reported as a note.
--strict-network makes it exit 1 instead.
The project already has dozens of findings. Where do I start?¶
With a baseline: --generate-baseline records what is there today, and CI then
fails only on findings that are new or have got worse.
Why is this package flagged — or why is it not?¶
From 0.8.0 on, composer lockrot --explain=vendor/package prints that one package with everything
it was decided on: the verdict and priority, how the package is reached, every signal with its raw
data and the dates behind it, the composer.lock entry, the release branches the repository lists
with their dates, the repository activity, and the thresholds the run measured against. composer why
answers who requires it; this answers why lockrot judged it so. Exit 0, it does not gate —
Explaining one package.
A package is finished, not abandoned. How do I say so?¶
Interface packages, frozen polyfills and metapackages are not rot. psr/*, fig/*,
symfony/polyfill-* and a few more are on the built-in allowlist and report finished; for
anything else, add an entry with a reason to extra.lockrot.ignore —
the allowlist.
Read on¶
| Page | What is on it |
|---|---|
| What it reports | The nine verdicts, the signals behind them, and how priority is assigned |
| Configuration | extra.lockrot, every option, and the command-line flags |
| In CI | GitHub Actions, GitLab CI, SARIF, PR comments |
| Reading a report | What a report says, and the viewer that opens one |
| Baseline | Accept today's findings, fail on new and worsened ones |
| Install-time summary | What the plugin prints during install/update, and how to silence it |
| PHAR and self-update | Verified and signed download, PHIVE, self-update, the global-plugin alternative |
| Example run | The full 200-package report the sample above is cut from |
| Data sources | Composer repositories, the GitHub, GitLab and Bitbucket APIs, caching, --offline |
| Changelog | What changed, release by release |
| Weekly watch | What lockrot finds in the newest release of widely used PHP applications, and in projects created from scratch that morning — and how much of it composer audit names |
| Blog | Notes from building lockrot: dependency rot, Composer internals, release engineering |
Source and issues live on GitHub; the package is
somework/lockrot on Packagist. Released under
the MIT licence.