Safeguard
Developer Tools•

NuGet lock files and locked mode: repeatable restores in CI

Enable packages.lock.json with RestorePackagesWithLockFile, then use locked mode in CI so a restore either matches the lock or fails loudly.

Safeguard Security Team
3 min read

For repeatable .NET restores, set RestorePackagesWithLockFile to true so NuGet writes packages.lock.json, commit the file for applications, and run restore in locked mode in CI. Locked mode restores exactly what the file lists, and it fails if your declared dependencies no longer match.

Why restores can differ

NuGet tries to produce the same full closure of packages if your PackageReference list has not changed. The Microsoft documentation lists cases where it cannot:

  • Floating versions such as 4.*, which are meant to move to the latest version on restore.
  • A newer package version that matches your requirement is published. The example: you ask for 4.0.0, only 4.1.0 and later exist, so NuGet picks 4.1.0. Later 4.0.0 is published and restore starts picking it.
  • A version is removed from a repository that allows deletions.

Any of these can change a build with no change in your code.

Enabling the lock file

<PropertyGroup>
  <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>

Restore then writes packages.lock.json in the project root, listing all packages including transitive ones. Once the file exists, NuGet uses it on restore even without the property. A blank file also opts you in.

Normal and locked restore

With a lock file present, NuGet checks whether the dependencies in your project files changed. If not, it restores what the lock lists with no re-evaluation. If they did change, it re-evaluates and updates the lock.

For CI, turn on locked mode.

dotnet restore --locked-mode

In locked mode, restore installs the exact packages in the file or fails when your declared dependencies differ from it. You can also set RestoreLockedMode in the project file.

Updating on purpose

The documentation lists a force-evaluate option that makes restore re-evaluate floating versions. Use that deliberately, then commit the changed lock.

Applications and libraries

For an application, check in the lock file. For a library you do not ship, or common code that other projects depend on, Microsoft says you should not, because the consuming project's own lock decides the versions, not yours.

Watch the SDK

Some project types add implicit package references from the .NET SDK. A different SDK version can change those versions and cause locked restores to fail. The documentation suggests a global.json with the rollForward policy set to disable, which means every developer and CI must use the same SDK version.

A word on floating versions

Floating versions are convenient and are the main reason a lock file is worth having. If you keep them, treat every refresh as a reviewed change, not as something that happens by itself on a build agent.

Review habit

Treat packages.lock.json changes as code. A pull request that changes a PackageReference should show a matching, reasonable lock diff. A lock diff with no manifest change deserves a question.

Safeguard's SBOM and dependency scanning helps you see which locked packages carry known issues, and a free scan is at gold.safeguard.sh/scan.

Quick check

What does locked mode do on a mismatch?

It fails the restore instead of updating the lock file.

Do I need the property if the lock file already exists?

No. Once packages.lock.json is in the project root, NuGet uses it on restore.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.