Loading...
Skip to main content
Reference

Manifest vs Lockfile: What Each One Actually Controls in Your Dependency Tree

Sudhir P.
Last updated
7 min read

A manifest (package.json, requirements.txt, pom.xml) declares the dependencies you chose, usually as version ranges. A lockfile (package-lock.json, poetry.lock, Cargo.lock) records the exact, fully-resolved dependency graph — every direct and transitive package pinned to one specific version. Scanners read the lockfile, not the manifest, because it is the only file that says what is actually installed.

Two files sit at the root of almost every project, and developers routinely conflate what each one is for. package.json says "I depend on express version 4.x." package-lock.json says "on the last install, that resolved to express@4.19.2, which itself pulled in accepts@1.3.8, which pulled in mime-types@2.1.35" — and so on through every package your dependencies depend on. Confusing the two is why "but it works on my machine" and "the scanner found a CVE in a package I never installed" are the same underlying problem.

What We'll Cover

Manifest: What You Declare

A manifest is the file where you declare your project's direct dependencies, typically as a name plus a version range rather than an exact version. package.json for npm, requirements.txt or pyproject.toml for Python, pom.xml for Maven, go.mod for Go, Gemfile for Ruby, Cargo.toml for Rust, composer.json for PHP. Every ecosystem has one.

The manifest expresses intent, not fact. "express": "^4.17.0" in a package.json means "any 4.x release at or above 4.17.0 is acceptable" — it does not mean 4.17.0 is what's installed. That gap between "what the manifest allows" and "what's actually on disk" is precisely why the manifest alone can't tell a scanner, or a developer, what code is actually running.

Lockfile: What Actually Got Installed

A lockfile is the file your package manager writes after resolving the manifest's version ranges against the actual registry, recording the exact version of every package — direct and transitive — along with integrity hashes so the same install can be reproduced exactly. package-lock.json (or yarn.lock, pnpm-lock.yaml) for npm-ecosystem tools, poetry.lock or uv.lock for Python, Gemfile.lock for Ruby, Cargo.lock for Rust, composer.lock for PHP.

Go is the one ecosystem here that doesn't fit this two-file split cleanly. go.mod itself already records the exact selected version of every direct and indirect dependency (via Go's Minimal Version Selection algorithm) — there's no separate "resolved versions" file the way npm splits package.json from package-lock.json. go.sum is not a lockfile in that sense; it's a cryptographic checksum manifest that verifies the content of each module version hasn't been tampered with, and it doesn't get consulted to decide which version to use. Treat go.mod as playing both roles for Go, with go.sum as an integrity-verification companion rather than a version source.

npm's own documentation describes the purpose precisely: the lockfile "describes the exact tree that was generated, such that subsequent installs are able to generate identical trees, regardless of intermediate dependency updates." Two developers running npm install against the same lockfile get byte-identical dependency trees, even if the registry has published newer versions in the meantime. Without a lockfile, two installs a week apart can silently resolve to different transitive versions.

This is also why SBOM generators parse lockfiles rather than manifests — an SBOM is only accurate if it reflects what's actually installed, and only the lockfile records that.

The Dependency Graph: Direct vs Transitive

A dependency graph is the full set of package-to-package relationships a lockfile encodes: which packages your project declared directly, and which packages those packages pulled in on their own.

  • Direct dependency: a package your manifest names explicitly. You chose it, you can see it by reading package.json.
  • Transitive dependency: a package pulled in because one of your dependencies (or one of theirs) depends on it. You never named it, and most developers can't list more than a handful of their project's transitive dependencies from memory.

The ratio between the two varies enormously by ecosystem — see Transitive Dependencies: Why Clean Direct Deps Aren't Enough for the actual numbers across npm, Python, and Maven, and why the vulnerability count skews so heavily toward the transitive layer. This article stops at the mechanics: how the graph gets built in the first place.

Semantic Versioning: How Ranges Resolve to Exact Versions

Manifests almost always specify version ranges, not exact versions, and the ranges follow Semantic Versioning (semver): a MAJOR.MINOR.PATCH format where major versions signal breaking changes, minor versions add backward-compatible functionality, and patch versions fix bugs without changing behavior.

Range operators tell the resolver how much drift is acceptable:

RangeMeaningExample match
^4.17.0Compatible with 4.17.0 — any 4.x.x at or above4.19.2
~4.17.0Approximately 4.17.0 — patch-level changes only4.17.9, not 4.18.0
4.17.0Exact version, no drift allowed4.17.0 only
>=4.17.0 <5.0.0Explicit rangeanything in that window

The resolver walks every manifest in the tree — yours and every dependency's — and picks the version that satisfies the most constraints. That resolution step is where a lockfile gets generated: it's the frozen record of what the resolver decided, so the next install doesn't have to make the same decision again and risk a different answer.

Why This Matters for Vulnerability Scanning

A scanner that reads only the manifest sees "lodash": "^4.17.0" and has to guess which of the ~20 releases from 4.17.0 to 4.18.x you're actually running — it might miss a vulnerability in the version you actually have, or flag one in a version you've already moved past. A scanner that reads the lockfile sees lodash@4.17.15 exactly, plus every transitive package that pulled it in, and can match that precise version against vulnerability databases like OSV, NVD, or the GitHub Advisory Database with no ambiguity. GeekWala's scanner accepts lockfiles for exactly this reason, and also takes manifests such as package.json, requirements.txt, and pom.xml when that is all you have — it reads the versions in the file you give it and does not resolve the dependency tree itself, so only a lockfile (or SBOM) brings the transitive packages. Step-by-step guides cover the two most common cases: checking package-lock.json and scanning requirements.txt, Pipfile.lock, or poetry.lock.

This is also why "no lockfile committed" is itself a finding worth flagging, independent of any specific CVE: without one, two installs of the same manifest can produce two different dependency graphs, and a scan of one tells you nothing certain about the other.

Manifest and Lockfile Formats by Ecosystem

EcosystemManifestLockfile
npm / Node.jspackage.jsonpackage-lock.json, yarn.lock, pnpm-lock.yaml
Pythonrequirements.txt, pyproject.tomlpoetry.lock, uv.lock, Pipfile.lock
Java / Mavenpom.xml(Maven resolves at build time; use mvn dependency:tree for a snapshot)
Gogo.mod (also holds resolved versions)go.sum (checksums only, not a version source)
RubyGemfileGemfile.lock
RustCargo.tomlCargo.lock
PHP / Composercomposer.jsoncomposer.lock
.NET / NuGet.csproj (PackageReference)packages.lock.json

Frequently Asked Questions

Should I commit my lockfile to version control?

Yes, for applications. Committing the lockfile is what makes builds reproducible across your team and CI — without it, everyone (and every pipeline run) resolves versions independently, and a "works on my machine" bug can trace back to two developers having different transitive versions of the same package. Libraries published for others to depend on are the one common exception, since the consuming project's own resolver will make the final call regardless.

Does deleting node_modules and reinstalling change my lockfile?

It shouldn't, if the lockfile is present before you reinstall — package managers resolve against the existing lockfile first and only deviate from it when the manifest itself changed in a way the lockfile can't satisfy. Deleting the lockfile itself is what triggers a fresh resolution, which can shift transitive versions.

Can two projects with identical manifests have different lockfiles?

Yes — if they were resolved at different times against a registry that published new versions in between, or with different package-manager versions that resolve ranges slightly differently. This is exactly why the lockfile, not the manifest, is the source of truth for what's actually running.

Where does a scanner without lockfile support fall short?

It has to either resolve the manifest's ranges itself (slow, and it may not match your actual installed tree) or scan only direct dependencies (missing the transitive majority — see Transitive Dependencies for how large that majority usually is). Lockfile-aware scanning is the baseline for accurate results, not an optional enhancement.