Skip to content

Baseline

Record the findings you have already seen and decided to live with, so CI fails only on what is new or has got worse since:

composer lockrot --target-php=8.4 --generate-baseline
git add lockrot-baseline.json && git commit -m "chore: accept current dependency rot"

A large project rarely starts clean, and turning --fail-on off loses the check entirely. The baseline is the middle ground.

What --generate-baseline does

It writes lockrot-baseline.json next to composer.json, prints one line on stderr (lockrot: baseline written to lockrot-baseline.json (74 findings)), nothing on stdout, and exits 0 whatever --fail-on says — the run records findings, it does not judge them.

Commit the file. It is a statement about the project, and it is worth reviewing in a pull request like any other change. It is also the only file lockrot ever writes, and only on this explicit flag.

--strict-network is the one exception to that exit 0: if a configured repository or GitHub could not be reached, the run still exits 1 after writing the file. A baseline generated from metadata that never arrived would accept findings lockrot was not actually able to check.

{
    "lockrot": {
        "version": "0.2.1",
        "schema": 1
    },
    "generated_at": "2026-09-14T00:00:00+00:00",
    "findings": {
        "behat/transliterator": {
            "version": "v1.5.0",
            "verdict": "abandoned",
            "first_seen": "2026-09-14"
        }
    }
}

Only flagged verdicts are recorded (ok, finished and unknown are not findings), entries are sorted by package name so the diff stays reviewable, and first_seen is carried over when you regenerate — the file keeps saying how long each finding has been tolerated.

Reading a baseline

Once the file exists, every normal run compares against it and says so:

200 packages checked · abandoned 19 · silent 8 · pinned 4 · old-promise 41 · stale 2 · unknown 0 · finished 18 ·
ok 108
priority: critical 3 · high 58 · medium 11 · low 2
baseline: 72 known · 1 new · 1 worsened · 0 stale (lockrot-baseline.json)
Bucket Meaning Effect on the exit code
known The baseline holds this package at this verdict or a worse one Never fails the build. The table shows abandoned (baseline), annotations drop to notice/note
new Flagged now, absent from the baseline Compared against --fail-on as usual
worsened In the baseline, but at a lower verdict than today's Compared against --fail-on. The table shows abandoned (was stale)
stale In the baseline, no longer in composer.lock Never fails the build. Reported as a note so you know the entry can go

Matching is by package name only. The recorded version is informational, so bumping vendor/pkg from 1.2.3 to 1.3.0 while it stays abandoned keeps it accepted; a package that gets worse (staleabandoned) is reported as worsened and fails the build again. Stale entries are never cleaned up behind your back — regenerate the baseline when you want them gone.

Generate it with the same --dev setting your CI run uses

--dev widens what is analysed, not what counts as present: staleness is measured against the whole composer.lock, packages-dev included, so a baseline generated with --dev never reports its development entries as stale on a run without it.

The other direction does matter. A baseline generated without --dev contains no development findings, so a --dev run reports every one of them as new and fails.

When lockrot cannot read the file

A baseline lockrot cannot read is a configuration error, not an absent baseline: a malformed or schema-invalid file, or a --baseline/extra.lockrot.baseline path that does not exist, exits 2 rather than silently running ungated. The default path simply not existing is not an error — that is every project before its first --generate-baseline. To start over from a file that has been damaged, delete it and generate a new one.

Install time

install-time-strict uses the same comparison: a finding the baseline already carries does not stop a composer require. The compact block still lists it.

Install time never fails on a configuration problem, so it handles an unreadable baseline differently from composer lockrot. An extra.lockrot.baseline pointing at a missing or unreadable file becomes the usual single lockrot: install-time check skipped: … line, and because the check was skipped the install-time-strict gate does not run for that install either. Fix the path or remove the key — composer lockrot reports the same problem as exit 2 and is the quicker way to see it.

  • configuration.mdbaseline and --baseline=<path>
  • ci.md — exit codes and how each format renders a baseline-known finding
  • install-time.md — the compact block and install-time-strict