Every existing developer's install works, but a new hire's npm ci fails with npm error notarget No matching version found for @acme/telemetry@^2.4.1, a fresh pip install -r requirements.txt pulls urllib3 2.x and breaks with ImportError: cannot import name 'DEFAULT_CIPHERS', or bundle install resolves a different nested gem version than anyone else has. Old machines have cached, resolved trees; a fresh clone resolves from scratch against today's registry. The difference lives in transitive dependencies — the dependencies of your dependencies — that nobody chose directly. This page finds them and locks them down, as part of dependency tree visualization.

The most reliable way to catch these failures is to do what a new hire does: install from nothing, on a clean machine, regularly.

Diagnostic

Install from scratch in a clean container and compare the resolved tree with what the lockfile says:

#!/usr/bin/env bash
set -euo pipefail
docker run --rm -v "$PWD":/src:ro -w /tmp/app node:20-bookworm-slim sh -c '
  cp -r /src/package.json /src/package-lock.json . && npm ci --no-audit --no-fund 2>&1 | tail -3'
git diff --stat package-lock.json
docker run --rm -v "$PWD":/src:ro -w /tmp/app python:3.12-slim sh -c '
  cp /src/requirements.txt . && pip install -q -r requirements.txt 2>&1 | tail -2; pip freeze | grep -i urllib3'
grep -nE '^[a-zA-Z0-9_.-]+$' requirements.txt | head -5 || echo "all requirements pinned"

Expected bad output:

npm error notarget No matching version found for @acme/telemetry@^2.4.1.
npm error notarget In most cases you or one of your dependencies are requesting
npm error notarget a package version that doesn't exist.
urllib3==2.2.3
3:requests
7:boto3

The npm lockfile references a version that was unpublished from the internal registry, and the Python requirements list top-level packages without versions, so urllib3 floats to a major version the code does not support.

Where Transitive Versions Come From Layers from direct dependencies declared by the team down to transitive ones resolved by the package manager. Where Transitive Versions Come From manifest direct deps you chose direct dep ranges ^2.4 or >=1.0 transitive deps chosen by resolver registry state today's versions lockfile freezes all layers
The lower layers change without anyone editing the manifest unless a lockfile pins them.

Root cause

Package managers resolve a dependency tree from version ranges. Direct dependencies are usually visible and reviewed; transitive ones are chosen by the resolver based on whatever the registry offers at install time. A lockfile freezes the whole tree, but only if it exists, is committed, and is honoured — npm ci honours package-lock.json, npm install may update it; pip install -r requirements.txt with unpinned lines has no lock at all. Existing machines hide the problem because they installed months ago and keep their resolved tree in node_modules or a virtualenv. A fresh clone resolves against the registry now, where a transitive dependency may have released a breaking version, been yanked, or been removed from an internal mirror. The failure appears only for new hires and clean CI caches, which is why it is so often misread as "their machine is broken".

Resolution

  1. Lock every ecosystem completely and commit the lockfile:
#!/usr/bin/env bash
set -euo pipefail
npm install --package-lock-only
python3 -m pip install --quiet pip-tools==7.4.1
pip-compile --generate-hashes --output-file requirements.lock requirements.in
git add package-lock.json requirements.in requirements.lock

requirements.in lists direct dependencies; pip-compile resolves and pins the full tree, with hashes, into requirements.lock.

  1. Install only from lockfiles in bootstrap scripts, Dockerfiles and CI — npm ci, pip install --require-hashes -r requirements.lock, bundle install --frozen — so nothing resolves at install time.

  2. Find what pulled a problem package in with the ecosystem's tree tools:

#!/usr/bin/env bash
set -euo pipefail
npm ls @acme/telemetry --all 2>/dev/null | head -10
python3 -m pip install --quiet pipdeptree==2.23.1 && pipdeptree --reverse --packages urllib3

The reverse tree shows which direct dependency requires the transitive package, which is the one to upgrade, pin or override.

  1. Override a bad transitive version when the direct dependency has not released a fix:
{
  "overrides": {
    "@acme/telemetry": "2.4.3"
  }
}

npm overrides (in package.json) force a version anywhere in the tree; in Python, add a constraint line to requirements.in (urllib3<2) and recompile.

  1. Mirror what you depend on. If an internal registry proxies public packages, configure it to cache rather than proxy-through, so an unpublished upstream version does not break installs.
Why Only Fresh Installs Break Flow contrasting an old machine's cached tree with a fresh clone resolving against today's registry. Why Only Fresh Installs Break fresh clone no node_modules no or loose lock ranges only resolver today new or missing install fails only for new hire
The lockfile makes a fresh install produce the same tree as every other machine.

Expected output

$ docker run --rm -v "$PWD":/src:ro -w /tmp/app node:20-bookworm-slim sh -c 'cp /src/package*.json . && npm ci --no-audit --no-fund | tail -1'
added 1412 packages in 38s
$ docker run --rm -v "$PWD":/src:ro -w /tmp/app python:3.12-slim sh -c 'cp /src/requirements.lock . && pip install -q --require-hashes -r requirements.lock && pip freeze | grep urllib3'
urllib3==1.26.20
$ git diff --stat package-lock.json
$

A clean container installs exactly the locked tree for both ecosystems, and the lockfile is unchanged by the install.

That last point is the check that separates a real lockfile from a decorative one. If a clean install modifies the lockfile, some version was resolved at install time, and the next new hire may get a different tree again. npm ci fails outright in that case, which is the behaviour you want; for other ecosystems, a git diff --exit-code on the lockfile after installing in CI provides the same guarantee.

Prevention

  1. Run a scheduled clean install in CI with no caches, in a fresh container, nightly. It fails the day a transitive dependency breaks rather than the day a new hire joins:
name: fresh-install
on:
  schedule: [{ cron: '0 5 * * *' }]
jobs:
  install:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - run: docker run --rm -v "$PWD":/src:ro -w /tmp/app node:20-bookworm-slim sh -c 'cp /src/package*.json . && npm ci'
      - run: docker run --rm -v "$PWD":/src:ro -w /tmp/app python:3.12-slim sh -c 'cp /src/requirements.lock . && pip install --require-hashes -r requirements.lock'
  1. Reject unpinned requirements in review: a CI grep that fails on lines in lockfiles or requirements.lock without ==.

  2. Update dependencies deliberately with Renovate or Dependabot pull requests, so lockfile changes are reviewed and tested instead of happening implicitly during someone's install.

Where Fresh-Install Failures Came From Bar chart of causes of fresh-install failures found by a nightly clean-install job over six months. Where Fresh-Install Failures Came From unpinned transitive major 7 yanked or unpublished 4 registry mirror gap 3 direct dependency change 1
Illustrative counts; almost all were transitive changes no one on the team made.

Platform caveats

Apple Silicon (ARM64): lockfiles pin versions, not platform binaries; a locked package can still lack an arm64 build. The clean-install job should run on both architectures if the team uses both — see diagnosing native module build failures on ARM64.

Monorepos: workspace managers (npm, pnpm, Yarn) keep one lockfile at the root; running installs in sub-packages can create stray lockfiles. See resolving duplicate lockfile versions in a monorepo.

Windows (native): pip-compile --generate-hashes output is platform-neutral for pure-Python packages but may list platform-specific wheels; compile on Linux in CI if production runs Linux.

Rollback

Lockfiles and overrides are ordinary files; revert them to return to range-based resolution:

#!/usr/bin/env bash
set -euo pipefail
git checkout HEAD~1 -- package.json package-lock.json requirements.txt
git rm --cached -q requirements.in requirements.lock 2>/dev/null || true

Frequently Asked Questions

Why does the install fail for a new hire but work for everyone else?

Everyone else installed months ago and still has a resolved tree on disk. The new hire resolves from scratch against today's registry, where a transitive package changed or disappeared. A committed lockfile and lockfile-only installs remove the difference.

What is the difference between npm install and npm ci?

npm ci installs exactly what package-lock.json specifies and fails if it does not match package.json; npm install may update the lockfile. Use npm ci everywhere except when intentionally changing dependencies.

How do I find which dependency pulled in a broken package?

Use a reverse tree: npm ls <package> --all for Node, pipdeptree --reverse --packages <package> for Python, bundle exec gem dependency -R <gem> for Ruby.

Are overrides safe to keep long term?

They are a stopgap. Keep a comment or issue explaining each override and remove it once the direct dependency releases a fixed version, or it may pin a vulnerable version forever.