Dependency research · 13

Do popular packages ship what is in their source?

Reviewers read git. Users install from the registry. We compared the two for the 1,000 most-downloaded npm and PyPI packages. Where the comparison could be made, 98% matched. For about one release in thirteen, it could not be made at all.

Measured 12 Oct 2026 · corrected 9 Nov 2026 · 1,000 packages, 827 compared · npm and PyPI · depproof research

Correction, 9 November 2026. The first version of this study said 133 releases (13%, one in eight) could not be checked against their source. Our tag and repository-link matching missed common naming forms. Re-measured against each repository’s full tag list and each release’s registry metadata, the figure is 77 (about 8%, one in thirteen). The 98% match rate holds with the newly checkable releases added. The finding stands; its size was overstated.

Two copies of every release

Every package release exists twice. There is the source in a public repository, which is what code review, security audits and curious engineers read. And there is the artifact on the registry, which is what npm install and pip install actually fetch. Most of the time nobody checks that the two agree, and that gap is part of the application security risk that sits in your dependencies.

They do not have to be identical. Packages legitimately ship build output that is not in git. But some things decide what runs on your machine at install, and should not change between the two without a reason: the scripts a package runs on install, the dependencies it pulls in, the commands it adds, and on PyPI the .pth files that run at every Python start.

That matters for a practical reason. Several widely reported registry compromises were published straight to the registry with a maintainer's stolen credentials, with no matching change in git. A release that declares something its source does not is a sensible place for review to start.

What we compared

The 500 most-downloaded packages on npm and the 500 most-downloaded on PyPI, each at its latest release on 12 October 2026. For each, we took the artifact users install and the source at the git tag for that release, and compared what each declares at install level: install scripts, dependencies and command-line entry points, and on PyPI also .pth files and setup.py.

Where it can be checked, it almost always matches

Of the 827 releases that could be compared reliably, 810 declare nothing at install level that their source does not. That is 98%. Fifteen of those differ only in fields that do not affect what happens at install.

npmPyPITotal
Matches at install level432378810
Differs, ordinary build-time explanation4913
Differs, not attributed automatically044
Compared reliably436391827
No repository link (corrected 9 Nov: 0 · 24 · 24)24042
No git tag for the release (corrected 9 Nov: 37 · 16 · 53)523991
Several packages in one repository01212
Package not found in its repository at the tag707
Source too large to fetch, or fetch error31821
All packages5005001,000

For 117 PyPI packages that declare their dependencies only inside an executable setup.py, the dependency comparison could not be made; their other checks ran. 110 of them are in the "matches" row. Leaving them out entirely, 700 of 710 match, which is also 98%.

The table shows the 12 October run. The 53 releases our first matching wrongly counted as untraceable were compared on 9 November: 45 match, 2 differ only by a setup.py generated at build time, 1 comes from a repository that publishes several packages, and 5 were too large to fetch. With them added, 855 of 874 releases compared reliably match, which is still 98%, with no new unexplained difference.

The 17 that differ, and why most of them do

Seventeen releases declare something at install level that their source does not. For 13 the cause is an ordinary step in how the package is built and published:

  • Platform binaries added at publish (4, npm). Packages with native code publish one binary per operating system and processor, and list them as dependencies only in the published manifest. rollup, for example, declares a binding for each platform in the release users install, a list its source manifest does not carry.
  • setup.py generated by the build tool (6, PyPI). Some packaging tools write a setup.py into the published source distribution that the repository never contained. It runs during installation from source.
  • Startup hook generated at build (3, PyPI). A .pth file runs every time Python starts. setuptools ships distutils-precedence.pth, created by its build rather than committed to git. Long-standing and documented.

The remaining 4 were not attributed automatically. We have no evidence either way about them, and they are under review. We are not naming them, because an unexplained difference is a question, not a finding.

The useful observation is the first category, not the last. Install scripts and binaries added at publish time are normal practice, and they are also exactly the part of a release that a reviewer reading git never sees.

About one in thirteen cannot be checked at all

The larger finding sits before the comparison. 77 of the 1,000 releases cannot be checked against their source, because the package publishes no source-repository link (24) or the repository has no git tag for the published version (53). That is about 8%: 37 on npm and 40 on PyPI. Up to five more sit on code hosts whose tags we could not read. (The first version of this study said 133; see the correction above.)

That is not evidence of anything wrong. Plenty of projects do not tag every release, and large repositories that publish many packages often tag the repository rather than each package. But for those releases there is no path from what people install to a reviewable commit, and nobody outside the project can say which source a given release was built from.

What this changes

For most popular packages, reviewing the source is a fair proxy for what you install, at least at the level of what runs during installation. That is worth knowing, and it is good news for anyone assessing application security risk in third-party code.

It stops being true in two places. For about one release in thirteen there is no source to review against, so the evidence has to come from the artifact itself, or from what the package does once it is installed. And where a release does differ, the difference sits exactly where install-time behaviour lives. A review process that reads only git sees neither, and a scanner that matches CVEs will not flag it either, because there is no CVE to find. That makes it a software supply chain question as much as an AppSec one.

What this does not prove

  • A match is not a clean bill of health. We compared what each release declares at install level, not the contents of every file. A change inside a file that is legitimately built at publish would not show here.
  • A difference is not an accusation. Thirteen of 17 have ordinary build-time causes; the other four have no evidence either way.
  • Some repositories could not be compared fairly. Where one repository publishes several packages, we compared against the repository root and reported the result as not comparable (12). Very large sources were not checked (17).
  • Dependency comparison was not possible for 117 PyPI packages that declare dependencies only in an executable setup.py. Their other checks still ran.
  • The sample is the top 500 per registry by downloads, one release each, on one day. Rankings came from ecosyste.ms for npm and hugovk/top-pypi-packages for PyPI. The long tail may look different.

The part we have not solved

If about one release in thirteen cannot be traced to its source, what does a reasonable team do with those, short of refusing to use them? Some things we would like argued with: should registries require a repository link and a matching tag? Should a published-only dependency list, like a set of platform binaries, be shown to reviewers explicitly? And if you maintain one of these packages, have we described a difference wrongly?

Where depproof fits

depproof records what each dependency actually does during your own installs and tests, so the evidence does not depend on whether its source can be traced.

If you re-run this and get something different, we would rather hear it. hello@depproof.com.

Related reading: managing application security risk in dependencies · what software supply chain security covers · nobody left to fix it · what your SBOM cannot say

Questions people ask

Is a package that differs from its source an application security risk?

It is where application security review should start, not a verdict. Most differences have ordinary build-time causes. But install scripts, dependencies and startup hooks decide what runs on your machine when a package is installed, and a vulnerability scanner that matches CVEs does not look at them. In our comparison of the 1,000 most-downloaded npm and PyPI releases, 17 of the 827 that could be compared declared something at install level that their source did not, and 13 of those had ordinary build-time explanations.

Can an npm or PyPI package differ from its source code on GitHub?

Yes. The registry artifact and the repository are published separately, and nothing forces them to agree. Most differences are ordinary build output. In our comparison of the 1,000 most-downloaded npm and PyPI releases, 98% of those that could be compared declared nothing at install level that their source did not.

How do I check that a package matches its source?

Find the repository the package links to, find the git tag for the exact version you install, and compare what each declares: install scripts, dependencies and command-line entry points, and on PyPI any .pth files and setup.py. If there is no repository link or no tag for that version, the release cannot be checked against source at all.

Why do some npm packages list dependencies that are not in their repository?

Packages with native code often publish one binary per operating system and processor, and add those binaries as dependencies only in the published manifest. It is legitimate, and it is exactly the part of a release that someone reading the repository does not see.

What is a .pth file in Python and why does it matter?

A .pth file in site-packages is read every time Python starts, and lines that begin with import are executed. Some are generated at build time rather than committed to git; setuptools ships one called distutils-precedence.pth. Because they run on every start, they are worth knowing about when you review a dependency.

What does it mean if a release has no git tag?

There is no reliable way to tell which commit it was built from, so nobody outside the project can compare it with reviewable source. It is not evidence of anything wrong, since many projects do not tag every release, but it means the evidence for that release has to come from somewhere other than the repository.

Cite this

About one in thirteen of the most-downloaded npm and PyPI releases cannot be checked against their source at all.

77 of 1,000 · 8% · no source-repository link or no matching git tag · top 500 per registry · measured 12 Oct 2026, corrected 9 Nov 2026 (first published as 133, 13%)

Where the comparison can be made, 98% of popular releases declare nothing at install time that their source does not.

810 of 827 compared reliably on 12 Oct; 855 of 874 with the 9 Nov re-check · 13 of the 17 differences in the first run have ordinary build-time causes · same sample

depproof research, “Do popular packages ship what is in their source? Comparing the top npm and PyPI releases with their tagged source,” Dependency research 13, 12 October 2026, corrected 9 November 2026. Each registry artifact was compared with the source at the git tag matching its published version, at manifest level. Figures and tables may be reproduced with attribution and a link back. No permission needed. Thirteenth in a series of dependency measurements.