somework / lockrot
Finds abandoned, unmaintained and branch-pinned packages in composer.lock. Composer plugin and standalone PHAR with CI exit codes.
Package info
Type:composer-plugin
pkg:composer/somework/lockrot
Requires
- php: ^7.4 || ^8.0
- composer-plugin-api: ^2.2
Requires (Dev)
- composer/composer: ^2.2
- friendsofphp/php-cs-fixer: ^3.70
- nikic/php-parser: ^5.1
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^9.6 || ^10.5 || ^11.5
- symfony/console: ^5.4
- symfony/process: ^5.4
Suggests
- ext-openssl: Lets lockrot.phar self-update verify release signatures; checked at runtime, the plugin itself never needs it
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.13.0
- v0.12.0
- v0.11.0
- v0.10.0
- v0.9.0
- v0.8.0
- v0.7.0
- v0.6.1
- v0.6.0
- v0.5.0
- v0.4.0
- v0.3.0
- v0.2.2
- v0.2.1
- v0.2.0
- v0.1.0
- dev-release/0.14.0
- dev-release/full-check-91c675d
- dev-feat/0.14-pr4a-flags-score
- dev-feat/0.14-pr3-release-scan
- dev-feat/0.14-pr2-advisory-lookup
- dev-release/pr2-probe
- dev-chore/0.14-follow
- dev-test/0.14-clean-tests
- dev-docs/0.14-clean-docs
- dev-docs/0.14-clean-src
- dev-release/clean-tests-probe
- dev-ci/0.14-infection-speed
- dev-release/inf-probe
- dev-docs/0.14-writing-rules
- dev-chore/infection-equivalents-lines
- dev-open-vocabularies
- dev-exposure-rule
- dev-pinned-release-facts
- dev-branch-php-admission
- dev-root-package
This package is auto-updated.
Last update: 2026-10-07 17:10:59 UTC
README
lockrot finds the packages in composer.lock that nobody maintains any more, including the ones
composer audit passes because no maintainer marked them abandoned.
Each finding carries its evidence and the dependency chain that pulled it in. lockrot reads
composer.lock and composer.json and writes neither.
composer require --dev somework/lockrot
composer config allow-plugins.somework/lockrot true
composer lockrot
See a full recorded run on a real project's lock →
Contents
Install · First run · What it reports · In CI · Not every finding is a problem · Configuration · Documentation · Limitations · Roadmap · Contributing · Security · License
Install
The Composer plugin needs PHP 7.4+ and Composer 2.2+ and adds composer lockrot (alias
composer rot):
composer require --dev somework/lockrot
composer config allow-plugins.somework/lockrot true
The composer config line grants the plugin permission Composer asks for.
The plugin also prints a short summary of the flagged packages a composer require, update or
install changes; set extra.lockrot.install-time to off to silence it
(Install-time summary).
The standalone PHAR needs only PHP 7.4+ and adds nothing to your project:
curl -fsSL -o lockrot.phar https://lockrot.dev/lockrot.phar php lockrot.phar -d /path/to/project
- Verify the download:
phive install somework/lockrotdownloads the PHAR and checks its GPG signature in one step. The sha256, GPG and build-attestation checks by hand are in Verifying the download. - Update it:
php lockrot.phar self-update(Keeping it updated).
First run
Goal: see what in your lock is unmaintained, why, and how to act on it. You need the plugin
installed and, for a complete run, a GitHub token in GITHUB_TOKEN
(Repository hosts and credentials).
-
Run it:
composer lockrot
lockrot checks against
config.platform.php, else the PHP running Composer. Pass--target-php=<version>when that is not the PHP your project runs on in production.Flagged findings are grouped by priority. Each row gives the verdict, the package, how it is reached (
directorvia …) and the evidence. The summary block under the list counts every verdict, sums the libyears, and names the direct requirements that pull in the most flagged packages.Recorded on a real project; the full wallabag run as text.
-
Ask why one package is flagged, or why it is not:
composer lockrot --explain=vendor/package
Prints the package's verdict, priority and every signal with its dates (Explaining one package).
-
Accept what you have decided to live with, passing the options your CI step uses:
composer lockrot --generate-baseline
Commit the
lockrot-baseline.jsonit writes; later runs fail only on findings that are new or worse (Baseline). -
Fail the build on the findings that matter to you:
composer lockrot --fail-on=high
Exits
1when a finding that is new or worse than the baseline has priorityhighorcritical. Choosing a threshold says when to pick another.
Next: What it reports for each verdict, every configuration key, and CI setup.
What it reports
Each package gets one verdict:
| Verdict | Meaning |
|---|---|
abandoned |
Its Composer repository marks it abandoned, or its repository is archived on GitHub or GitLab |
silent |
No release and no push to its repository in years |
pinned |
Installed from a branch snapshot (dev-main or any other dev-* branch, 2.x-dev), or the package has no tagged release |
left-behind |
The installed release branch stopped releasing while a newer branch still releases |
old-promise |
Released before the target PHP major existed, and admits it only because require.php has no upper bound |
stale |
An old release or an old push, short of silent |
unknown |
No repository metadata: not from a Composer repository, not listed, or the lookup failed |
finished |
On the built-in or project allowlist: complete by design |
ok |
None of the above |
- Priority (
criticaltolow) says how much a finding matters to this project: it starts from the verdict and drops for a transitive or development-only package (Priority). - Security advisories: on a flagged finding, lockrot names the release that fixes each
advisory
composer auditreports, when the repository lists one. On anabandoned,silentorleft-behindpackage with no fix it saysno fix expectedand raises the priority one step (Security advisories). - Transitive exposure: a transitive finding names every direct requirement that reaches it, and each direct requirement's evidence lists the flagged packages it pulls in (Transitive exposure).
- Libyears: the summary's
libyears:line sums, over the lock, the years between each installed release and the package's newest release (Libyears).
What it reports: every verdict, signal and threshold →
In CI
Run the same command as a CI step, with GITHUB_TOKEN in the step's environment:
- name: lockrot run: composer lockrot --fail-on=high --target-php=8.4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--fail-ontakes a verdict (fail on what was observed), a priority (fail on how much it matters here) orunchecked(fail when a check could not run).- Exit codes:
0passed,1a finding reached--fail-on(or, with--strict-network, a network lookup failed),2lockrot could not run or finish (Exit codes). - Formats:
--formattakestable(default),json,github(annotations),sarif(code scanning),gitlab(Code Quality),markdown(PR comment) orhtml(one self-contained page). Each--output=<format>:<path>writes one more from the same run (Writing reports to files).
Choosing a threshold and what each format gives: In CI.
On GitHub Actions, somework/lockrot-action v1 runs lockrot in one step:
- uses: somework/lockrot-action@v1 with: target-php: '8.4' fail-on: high
On other CI systems, use the PHAR or the Docker image ghcr.io/somework/lockrot
(The Docker image).
Not every finding is a problem
- Baseline: accept findings with
--generate-baseline(First run, step 3; Baseline). - Allowlist: packages finished by design (
psr/*,symfony/polyfill-*and others) report asfinished. Add your own underextra.lockrot.ignore, each with apackageand areason(The allowlist).
Configuration
Settings live under extra.lockrot in composer.json. A CLI option wins over an environment
variable, which wins over composer.json. The keys most projects set:
extra.lockrot key |
CLI option | Default | Effect |
|---|---|---|---|
fail-on |
--fail-on |
none |
Exit 1 threshold: a verdict, a priority, unchecked or none |
target-php |
--target-php |
config.platform.php, else the running PHP |
The PHP version the project runs on: decides old-promise, and which newer branch a left-behind finding tells you to move to |
include-dev |
--dev |
false |
Also check packages-dev |
format |
--format |
table |
Output format |
ignore |
— | [] |
Project allowlist |
A key lockrot does not know gets one warning line on stderr and changes nothing (Unknown keys).
Every key, environment variable and option →
Documentation
The full documentation is at lockrot.dev. What changed in each release is in the changelog.
Limitations
- Hosts: repository activity comes from GitHub, GitLab (gitlab.com and
gitlab-domains) and Bitbucket Cloud only; GitLab's archived flag needs a token (Repository hosts and credentials). - Anonymous caps: without credentials for GitHub or Bitbucket, the host is asked only about packages already stale on release age, up to a per-run cap; the report counts what it skipped (Repository hosts and credentials).
- Package metadata comes from the Composer repositories your project configures; a Satis build
needs
notify-batchset (Which Composer repository answers). - No inherited verdicts: a package is never flagged for what it depends on. Its evidence and the
pulled in by:line say what it pulls in (Transitive exposure). composer require --dev … --dry-run: the install-time summary can read a new package's priority one step high (Under--dry-run).
Roadmap
Planned and proposed work is tracked as enhancement issues.
Contributing
Bug reports, fixes and additions to the built-in allowlist are welcome; see
CONTRIBUTING.md. What counts as public interface, and the stability promise
for reports and options: Compatibility.
Security
To report a security issue, follow SECURITY.md rather than opening a public issue.
What lockrot reads, writes and contacts is in
What lockrot does and does not do.
License
MIT; see LICENSE. Written by Igor Pinchuk.
