Each package manager has one behavior that surprises people who know a different one. npm's clean install deletes node_modules. pip hash checking is all or nothing. Go prefers a vendor directory when one exists. NuGet treats a version as a minimum. Here is a one page list, each item from official documentation.
The cheat sheet
| Ecosystem | The gotcha | Why it matters |
|---|---|---|
| npm | The clean install command removes an existing node_modules first and errors if lock and manifest differ | Do not run it expecting to add a package |
| pip | Giving a hash to any requirement turns on hash checking for all, and every dependency must be listed and pinned | A half hashed file fails by design |
| Go | With go 1.14 or higher in go.mod and a vendor directory present, the go command uses the vendor directory by default | Edits to go.mod may not take effect until you re-vendor |
| Cargo | cargo install picks the latest dependencies unless told to use the lockfile | Installing a tool is not the same as building your project |
| NuGet | A plain version means that version or higher, preferring the lowest | A newly published lower version can change resolution |
| Gradle | Locking is opt in, and a changed direct version needs the lock updated or resolution may fail | Update lock state with the version |
| Maven | The nearest definition in the tree wins, and with a tie the first declaration wins | Depth in the tree decides, not newest |
| Composer | install uses composer.lock, update rewrites it | Run update only on purpose |
Notes on each
npm. The clean install never writes to package.json or any lock. Flags used to create the lock must be used again, or errors are likely.
pip. The hash fragment in PyPI download URLs only checks for corruption, since it comes from the same server, and does not satisfy the local hash requirement.
Go. go.sum is checked on every download. If a hash is missing, Go consults the checksum database unless the module is matched as private.
Cargo. The FAQ states that cargo install selects the latest dependencies unless locked is passed.
NuGet. The docs give a day by day example where publishing version 4.0.0 on day two changes what a requirement for 4.0.0 resolves to. Locking removes that.
Gradle. Lock state goes in gradle.lockfile in each project directory, and Gradle will not write lock state if the build fails.
Maven. You can guarantee a version by declaring it explicitly in your POM, and dependency management can set versions of transitive dependencies.
Composer. The lock records exact versions, so teammates, CI and production use the same set.
How to use it
Print this and place it where the pipeline owners sit. When a bug report says "it worked on my machine", scan the table for the tool in question. Many surprises are one of these eight behaviors.
When you add a language that is not on the list, write its row, using only what its official docs say.
Safeguard can help here: it scans dependencies and generates SBOMs, applies reachability analysis so you can sort findings by what your code can actually call, and enforces policies and gates on pull requests. You can try a free scan on a repository to see what it finds.
Quick check
Why does Maven's nearest definition rule matter for security?
A vulnerable version nearer the root can win over a patched one deeper in the tree. Declare the version you want explicitly.
Does Go always use the module cache?
Not if a vendor directory exists and go.mod says go 1.14 or higher. Then it acts as if vendor mode were on, unless told otherwise.
Sources
- npm ci
- pip secure installs
- Go modules reference
- Cargo FAQ
- NuGet PackageReference
- Gradle dependency locking
- Maven dependency mechanism
- Composer basic usage
Last reviewed: 19 October 2026.