AI coding

A Second Registry Check Before an Agent Bumps a Package

Antonio Evans10 min read

A split scene: on the left, a lockfile entry shown as a single line with a package name, version, and integrity hash.
Key points

Key points

  1. 1Before an agent edits a lockfile, it should fetch the registry record for the exact version and the maintainer set, then diff both against the current locked entry.
  2. 2A version that is yanked, deprecated or absent from the registry, or a maintainer set that changed, should block the bump and travel to the reviewer as evidence.
  3. 3Advisory databases report vulnerable ranges, not lockfile entries, so the agent must test the locked version against those ranges itself.
  4. 4The output should be a small evidence bundle (registry snapshot, advisory IDs, a diff of the changed fields) that a reviewer can audit in seconds.

What the lockfile proves, and what it leaves out

A lockfile records the exact versions that a resolver picked for every direct and transitive dependency, which is what makes a build reproducible and what integrity hashes are checked against. A study of lockfiles across seven package managers (npm, pnpm, Cargo, Poetry, Pipenv, Gradle and Go) describes them as a record of the resolved set, used to reduce build times, verify integrity and support reproducibility (Gamage et al., The Design Space of Lockfiles Across Package Managers). Pants' documentation makes the same point for Python: a lockfile pins transitive versions plus the expected SHA256 of each artifact, and the resolver only changes them when you deliberately ask it to (Pants docs, Lockfiles).

What the lockfile does not record is registry state. It tells you which version was resolved and what its hash was, not whether that version is still installable, whether its maintainers are the same people, or whether an advisory now covers it. That gap is exactly where a dependency bump can go wrong while every local check passes.

A second gap is drift between manifest and lockfile. If someone edits the manifest without regenerating the lockfile, CI that runs npm install silently fixes the mismatch while CI that runs npm ci fails on it, and deploys can run versions nobody tested (sbomify, What Is Lock File Drift). An agent that bumps a dependency should treat "lockfile still matches the manifest" as a precondition, not a bonus.

A third gap is provenance inside the lockfile itself. Liran Tal showed that a pull request which looks like an ordinary version bump can point one locked entry at a tarball hosted on a personal GitHub repository instead of the official registry, and a folded diff of several hundred lines hides it (Snyk, Why npm lockfiles can be a security blindspot). The registry check described here is the cheap way to catch that class of change before a human reads the file.

The four comparisons that decide the bump

The pre-review check is not one query. It is four comparisons, each with a pass condition and a block condition.

ComparisonSourcePassBlock and escalate
Version statusRegistry metadata for the exact versionVersion exists and is not yanked or deprecatedYanked, deprecated, unpublished, or missing
Maintainer setRegistry maintainers fieldSame set as the recorded baselineAny addition or removal
Advisory coverageAt least one advisory database plus vendor advisoriesNo advisory range includes the locked versionAny advisory range includes the locked version
Resolution fieldLockfile resolved and integrity for the entrySame host and registry as before, same hashHost change, missing integrity, hash change on an unchanged version

The first comparison is the one people skip. Yanking is not rare: a study of crates.io found that 9.6% of packages had at least one yanked release and 46% of packages had directly adopted at least one yanked release, with yanked releases propagating through the dependency graph (Li, Cogo and Bezemer, An Empirical Study of Yanked Releases in the Rust Package Registry). The same paper notes that only 5.3% of packages explained why a release was yanked, so the registry record usually gives you a status bit but no reason. That is precisely why the evidence bundle should carry the registry response verbatim instead of a summary: the reviewer needs to see that the reason field was empty.

The second comparison catches account changes. A maintainer set that grows by one account between two releases is a legitimate event most of the time, and a signal worth a second look the rest of the time. The agent does not need to judge intent; it needs to show the two lists side by side.

The third comparison is where most teams already have tooling, but the semantics are often misunderstood. Advisories describe ranges of affected versions. A lockfile entry is a single version. The agent has to test membership; a match on package name alone tells you nothing.

The fourth comparison is cheap and catches the tarball-substitution trick described above.

Detect yanked and deprecated versions

Query the registry for the package and the exact version the lockfile pins, and read the status fields the registry returns. For npm the packument exposes a deprecated field per version when the publisher has marked it, and the Cargo registry index removes yanked releases so resolution refuses them (Li et al.). Yanked and deprecated differ in force: a yanked release is often refused for new resolutions while an existing lock can keep installing it, whereas a deprecated release installs but warns. Both are review events, and they are not the same event.

A minimal check for npm-shaped registries, run against the version in the lockfile rather than the range in the manifest:

PKG=left-pad
VER=1.3.0
curl -fsSL "https://registry.npmjs.org/${PKG}" \
  | jq --arg v "$VER" '{version: $v,
      exists: (.versions[$v] != null),
      deprecated: (.versions[$v].deprecated // null),
      maintainers: [.maintainers[].name],
      dist: (.versions[$v].dist // null)}'

Record the command, the URL, the timestamp and the response body. That snapshot is the first item in the bundle.

One decision rule worth writing into the agent: a missing version is a block even if the package still exists. Registries allow unpublishing under policy, and a lockfile entry pointing at a version the registry no longer serves will fail installs in a clean environment. The failure is not reproducible on the machine that already has the package in its cache, which is how it reaches production.

Compare maintainer sets, not just maintainer counts

Two releases of the same package can be published by different accounts. The registry maintainers list is small and easy to diff, so diff it as a set, not a count, and store the baseline from the version currently locked.

baseline (1.3.0): ["alice", "bob"]
candidate (1.4.0): ["alice", "bob", "carol"]
status: BLOCK, maintainer added

A count-only check passes this example, and the added account is the entire signal. The reverse also matters: if a maintainer was removed between the locked version and the candidate, the release you are moving to was published by a smaller group, and the removal itself may be part of a takeover narrative the reviewer should see.

Do not attempt to score trust. The agent's job is to surface that the set changed and let the reviewer decide, because the reviewer has context the agent does not: whether the project has a published policy of adding maintainers, whether the new account has visible history, whether the change was announced. Presenting a diff is fast. Presenting a verdict invites the reviewer to skip the diff.

Test the locked version against advisory ranges

Advisory data is range-shaped, so the agent needs a range evaluator plus a list of sources. Use at least one general advisory database and, where one exists, the vendor's own advisory feed for the package ecosystem, because the two can disagree on coverage.

The evaluation has three outcomes, and only two are safe to automate:

  • No advisory mentions the package: pass, but record that zero sources returned data so a reviewer can tell "clean" from "the query failed".
  • The package is mentioned and no affected range includes the locked version: pass, record the advisory IDs as checked and not applicable.
  • The package is mentioned and an affected range includes the locked version: block, attach the advisory IDs and the affected range.

Semantic versioning helps here and stops helping at the edges. The specification's precedence rules handle pre-release and build metadata, with build metadata ignored for precedence and pre-release identifiers compared in a defined order (Semantic Versioning 2.0.0). So 1.4.0-rc.1 does not satisfy an affected range of >=1.0.0 <1.4.0 under standard range semantics, and an agent that compares with string ordering instead of a semver library will get this wrong at exactly the moment it matters.

There is a related trap the reviewer should see: bumping a transitive dependency can break consumers even when the intermediate package's public API is unchanged. A Maven scenario on Stack Overflow shows a project breaking with a linkage error because a transitive dependency had a breaking change while the intermediate library's own API stayed stable (Stack Overflow, Should I increase major version on dependency update). The semver change of the package being bumped is not the whole risk story; the registry and advisory check covers the cases semver labels cannot express.

Check drift and resolution changes before anyone opens the diff

Two fast checks belong in the bundle before any human opens the diff.

First, confirm the lockfile matches the manifest under a strict install. npm ci installs exactly what the lockfile specifies and fails when the lockfile and manifest disagree, unlike npm install, which regenerates the lockfile (sbomify). Python has the same idea: poetry check --lock verifies that the lockfile is current, and cargo check --locked refuses to update a stale Cargo.lock (sbomify).

Second, extract the exact entry and diff it against the base revision:

git diff -U0 origin/main -- package-lock.json \
  | grep -E '^[-+]\s+"(version|resolved|integrity)"' \
  | head -n 40

Restricting the output to the resolution-bearing fields keeps the bundle to a screen. The full lockfile diff can be thousands of lines, GitHub folds diffs past a couple of hundred lines by default, and nobody reads character diffs of machine-generated files (Snyk). The agent's job is to compress that diff into the fields that carry risk.

What about the manifest? The manifest declares ranges, and the lockfile records what the resolver chose; they are not two copies of the same information, and the lockfile is the one you should not hand-edit (Stack Overflow, Configuring package-lock.json to be source of dependency truth). The agent should edit the manifest, regenerate the lockfile with the package manager, then run the registry check on the newly locked versions.

One more scope rule. Lockfiles committed by a library are not used when that library is installed as a dependency, so a registry check on a library's own lockfile protects the maintainers and their CI, not downstream consumers (dev.to, When not to use package-lock.json). Agents should know which kind of repository they are in before deciding how much a lockfile check means. The same principle extends beyond package managers: GitHub's workflow lockfiles pin action commits because tags and branches can move, and the documentation notes that locking does not determine whether the dependency itself is safe, so review must happen before locking (GitHub changelog, Actions workflow lockfiles).

The evidence bundle a reviewer can audit in seconds

The pre-review check is worth running only if its output is smaller than the diff it replaces. Aim for one screen, fixed order, and machine-checkable fields.

bump: left-pad 1.3.0 -> 1.4.0  (direct)
manifest + lockfile: in sync (npm ci: ok)
registry snapshot: 2026-11-02T09:14:00Z, registry.npmjs.org/left-pad
version status:  1.4.0 exists, deprecated: null
maintainers:     baseline [alice, bob] -> candidate [alice, bob, carol]  BLOCK
advisory check:  OSV + vendor feed, 2 advisories mention package,
                 none include 1.4.0 -> checked, not applicable: CVE-x, GHSA-y
resolution:      same host, integrity present, hash changed with version
decision:        BLOCK on maintainer change; advisory clean; escalate

Four parts, each with a purpose. The registry snapshot answers "what does the registry say about this exact version, and when did we look." The maintainer diff answers "who can publish this package now." The advisory section answers "what was checked and what was not," including the zero-result case. The resolution diff answers "did the thing we will install change in a way the version number does not explain."

The decision line matters most. It forces the agent to commit to a rule, and rules are auditable in a way that prose is not. "Block if version status is not clean, maintainer set changed, any advisory range includes the locked version, or the resolution host changed" is a sentence you can test against a sample of merged pull requests. "Review dependencies carefully" is not.

This is also the natural place for a policy split. Low-risk repositories can auto-merge when the bundle is clean, which is what the Dependabot auto-merge pattern does once a required status check passes; the same tutorial stresses branch protection so the merge is gated by tests (TypeScript.tv, Never Review a Dependency Update Again). Repositories that ship to customers, or that have had a supply chain incident, can require a human on any maintainer change while still auto-merging advisory-clean patch bumps.

If your agents need to run this check inside a pipeline, Quanta Inference exposes text, image, video, audio and embedding models through one OpenAI-compatible key, which is enough to have a model read a registry response and produce the decision line without standing up a second vendor.

Worked example: three bumps, two blocked

Take a repository with three proposed bumps and run the check on each.

Bump A: left-pad 1.3.0 -> 1.3.1. The registry shows the version exists and is not deprecated. The maintainer set is unchanged. The advisory query returns zero matches, recorded as "0 advisories mention package" with both sources named. The resolution host is unchanged, integrity is present, and the hash changed along with the version. Decision: pass, auto-merge if CI is green.

Bump B: left-pad 1.3.1 -> 1.4.0. Everything passes except the maintainer list, which went from two accounts to three. Decision: block, escalate with the diff. The reviewer spends thirty seconds deciding whether the new maintainer was announced, instead of reading a six-hundred-line lockfile diff to find a field that is one line long.

Bump C: a transitive dependency moves because a direct dependency's range widened. The registry check shows the new version is fine, but the advisory query shows an affected range of >= 2.0.0 <2.3.2 and the newly locked version is 2.3.0. Decision: block with the advisory ID and the affected range quoted. This is the case where the direct bump looked clean and the resolver quietly selected a vulnerable transitive version, and it is the case that lockfile review by eye catches least often.

The arithmetic of the check is the argument for it. Four queries per changed package, one screen of output, and the reviewer's attention goes to the fields that changed rather than the lines that did. An agent that bumps ten packages and produces ten clean bundles has done work that would have taken a human an hour of scrolling.

FAQ

FAQ

What is the difference between a yanked version and a deprecated one?

A yanked release is usually refused for new resolutions but remains in the registry record, while a deprecated release still installs and typically shows a warning message. Both should block an unattended bump, but they need different messages to the reviewer: a yank usually means "do not use this," a deprecation usually means "there is a successor." Registries vary in how forcefully they enforce yanks, and only 5.3% of packages in one crates.io study explained why a release was yanked (Li et al.), so the agent must quote the registry record rather than paraphrase a reason.

How do I check advisories when the lockfile pins an exact version?

Advisories describe affected version ranges, so you evaluate membership: parse the locked version with a semver library and test it against each affected range. Record three outcomes separately: no advisory mentions the package, an advisory mentions it but no range includes the locked version, and a range includes it. The middle outcome is not the same as the first, and collapsing them hides query failures.

Can an agent auto-merge when the bundle is clean?

Yes, if the bundle is clean and a required status check passes, which is the branch-protection-plus-auto-merge pattern Dependabot workflows use (TypeScript.tv). The condition is that "clean" is defined before the agent runs, not after it sees the result. Start by requiring a human on any maintainer change and any advisory match, and widen auto-merge only after a sample of blocks proves to have been correct.

Does a semver change tell me whether a bump is safe?

No. Semver tells you what the publisher intended for their own public API, and a transitive dependency can still break consumers even when the intermediate library's API is unchanged, as the Maven linkage-error case shows (Stack Overflow). The registry and advisory check covers events the version number cannot express: yanks, deprecations, maintainer changes and advisory updates issued after publication.

What should the agent do when the registry query fails?

Block, and say so. A failed query is not a pass, and a bundle that silently omits a section is worse than one that reports an error, because the reviewer cannot tell whether the omission was checked. Record the failure with the URL and timestamp, treat the bump as unverified, and let the reviewer decide whether to retry or fall back to a manual check.

Sources

Sources

  1. The Design Space of Lockfiles Across Package Managers
  2. Lockfiles
  3. What Is Lock File Drift? A Hidden Risk in Dependency ...
  4. Why npm lockfiles can be a security blindspot for injecting ...
  5. An Empirical Study of Yanked Releases in the Rust ...
  6. Semantic Versioning 2.0.0 | Semantic Versioning
  7. Should I increase major version on dependency update
  8. Configuring package-lock.json to be source of dependency ...
  9. When not to use package-lock.json
  10. GitHub Actions workflow lockfiles in public preview
  11. Never Review a Dependency Update Again
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles