Validating lockfile consistency before push Jump to heading

A pull request bumps a version in package.json but not in package-lock.json. Locally it works, because node_modules already has the new version installed. CI runs npm ci, which refuses because the two files disagree, and the build fails ten minutes in — or, worse, CI uses npm install, quietly resolves a different tree, and tests pass against dependencies nobody reviewed. The same class of problem exists in every ecosystem: pyproject.toml and poetry.lock, go.mod and go.sum, Cargo.toml and Cargo.lock. Package managers can verify consistency in seconds without installing anything. Running that verification before push turns a slow, confusing CI failure into an instant, specific local message. This page builds the check for common ecosystems, within pre-push validation rules.

When to use this approach Jump to heading

  • CI regularly fails at the install step with lockfile mismatch errors.
  • Pull requests sometimes change a manifest without its lockfile, or the reverse.
  • Someone occasionally commits a lockfile from the wrong package manager — yarn.lock in an npm project.
  • Lockfiles are a frequent source of conflicts too; that side is covered in reducing lockfile churn in a busy repository.

Step 1 — Know the verification command for each ecosystem Jump to heading

Each package manager has a mode that checks the lockfile against the manifest without installing or modifying anything.

Consistency checks by ecosystemnpm can resolve the lockfile without installing using a dry-run of npm ci. pnpm and Yarn have frozen-lockfile modes. Poetry and uv check whether the lockfile matches the project file. Go verifies module sums and tidiness. Cargo can refuse to update the lockfile.CommandInstalls anything?npmnpm ci --dry-runnopnpm / Yarn--frozen-lockfile / --immutableresolves onlyPoetry / uvpoetry check --lock / uv lock --checknoGogo mod tidy -diffnoCargocargo metadata --lockednoall of these run in seconds — far cheaper than discovering the mismatch in CI
npm ci --dry-run --ignore-scripts >/dev/null           # npm
pnpm install --frozen-lockfile --lockfile-only         # pnpm
poetry check --lock                                    # Poetry
uv lock --check                                        # uv
go mod tidy -diff                                      # Go 1.23+
cargo metadata --locked --format-version 1 >/dev/null  # Cargo

Step 2 — Run only when manifests or lockfiles are in the push Jump to heading

The check is quick, but there is no reason to run it on pushes that touch neither file. Look at the files changed by the commits being pushed, using the helper from finding the new commits in a pre-push hook.

#!/bin/sh
# scripts/hooks/check-lockfiles.sh — reads pushed commit IDs on stdin
set -eu
changed=$(while read -r c; do git diff-tree --no-commit-id --name-only -r "$c"; done | sort -u)
need() { printf '%s\n' "$changed" | grep -Eq "$1"; }
fail=0
if need '(^|/)package(-lock)?\.json$'; then
  npm ci --dry-run --ignore-scripts >/dev/null 2>&1 || { echo "✗ package.json and package-lock.json disagree — run npm install"; fail=1; }
fi
if need '(^|/)(pyproject\.toml|poetry\.lock)$'; then
  poetry check --lock >/dev/null 2>&1 || { echo "✗ poetry.lock is out of date — run poetry lock"; fail=1; }
fi
if need '(^|/)go\.(mod|sum)$'; then
  go mod tidy -diff >/dev/null 2>&1 || { echo "✗ go.mod/go.sum not tidy — run go mod tidy"; fail=1; }
fi
exit $fail
A lockfile check that runs only when neededThe hook lists files changed by the commits being pushed. If any manifest or lockfile changed, it runs the matching ecosystem's consistency command, which resolves without installing. Mismatches stop the push with the exact command to fix them.Pushed commitschanged filesManifest touched?per ecosystemVerifyfrozen / dry-runPass or refusewith fix commandpushes that touch no manifest pay nothing

Step 3 — Reject lockfiles from the wrong package manager Jump to heading

A stray yarn.lock in an npm project, or package-lock.json in a pnpm project, means someone ran the wrong tool. Builds may pick the wrong file, and the stray file goes stale. Refuse them explicitly.

pm=$(sed -n 's/.*"packageManager": *"\([a-z]*\)@.*/\1/p' package.json)
case "$pm" in
  pnpm) bad='(^|/)(package-lock\.json|yarn\.lock)$' ;;
  yarn) bad='(^|/)(package-lock\.json|pnpm-lock\.yaml)$' ;;
  *)    bad='(^|/)(yarn\.lock|pnpm-lock\.yaml)$' ;;
esac
git ls-files | grep -E "$bad" && { echo "✗ lockfile from another package manager is tracked"; exit 1; } || true

Step 4 — Check monorepo workspaces as a whole Jump to heading

In workspaces, a single root lockfile covers every package. A version change in packages/api/package.json requires the root lockfile to change. Run the check from the root, and treat any workspace manifest as triggering it.

need '(^|/)package\.json$' && ( cd "$(git rev-parse --show-toplevel)" && pnpm install --frozen-lockfile --lockfile-only >/dev/null ) \
  || { echo "✗ workspace lockfile out of date — run pnpm install at the root"; fail=1; }
Why did the lockfile check fail?If a manifest changed without the lockfile, run the package manager to regenerate it. If the lockfile changed without a manifest change, someone may have edited it by hand or run a different tool version. If a lockfile from another package manager is tracked, delete it and use the declared one.What does the check report?manifest changed, lock notRegenerate locknpm/pnpm/poetry locklock changed, manifest notHand edit or tool skewpin the tool versionforeign lockfileWrong package managerdelete itpinning the package manager version prevents most of the middle case

Step 5 — Enforce the same check in CI Jump to heading

The pre-push check is fast feedback; CI is the enforcement. Run the identical script in CI so a push with --no-verify still fails before merge, and use the frozen install modes there as well.

jobs:
  lockfiles:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - run: git rev-list "origin/${{ github.base_ref }}..HEAD" | sh scripts/hooks/check-lockfiles.sh
      - run: npm ci                   # fails on mismatch rather than silently resolving

Never use npm install in CI: it updates the lockfile to match the manifest, hiding exactly the mismatch you want to catch.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Does npm ci --dry-run contact the registry? Jump to heading

It may fetch metadata to resolve the tree, but it does not install packages or run scripts. On a machine with a warm cache it is quick; offline, it can fail for reasons unrelated to consistency, so keep the CI check as the authority.

Why does the lockfile change when I run install, even though I changed nothing? Jump to heading

Usually a different package manager version writes the file differently. Pin the version with packageManager and Corepack, or the equivalent for your ecosystem, so everyone produces identical lockfiles.

Should dependency bots be exempt? Jump to heading

No. Bot pull requests should keep lockfiles consistent too, and the CI check catches a misconfigured bot quickly. See keeping lockfiles conflict-free during bulk updates.