Skip to content

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.

curl -fsSL -o lockrot.phar https://lockrot.dev/lockrot.phar
php lockrot.phar --target-php=8.4

Nothing is added to your project. For the checksum-verified download and how self-update works, see PHAR and self-update.

composer global require somework/lockrot
composer global config allow-plugins.somework/lockrot true

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:

lockrot in a terminal: one critical and four high findings — three of them left-behind, each with the constraint to require — then the medium and low rows, the summary counts by verdict and priority and the "Data as of" footer

What it looks for

  • abandoned — the package's own repository says so: Packagist carries the abandoned marker 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 its require.php constraint is open-ended (>=7.2), so Composer accepted it on a PHP nobody released it against. composer check-platform-reqs is satisfied too: >=7.2 is 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-only says 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 as composer require writes 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.