Set the RestorePackagesWithLockFile property to true so NuGet writes packages.lock.json, commit that file, and run CI restores in locked mode. Locked mode restores the exact packages in the lock file or fails if your project dependencies changed after the file was created.
What the lock file captures
NuGet's documentation says that with the property enabled, restore generates packages.lock.json at the project root, listing all package dependencies. That is the full closure, so it includes transitive packages, not only the ones you reference directly. The format is versioned, and the version depends on features you use, such as transitive pinning in Central Package Management.
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
There is a small quirk. Once a project has a packages.lock.json in its root, the lock file is always used for restore, even if the property is not set. The documentation notes that creating a blank file is another way to opt in.
Restore behavior
When a lock file is present, NuGet does a quick check for changes in the dependencies declared in the project files. If nothing changed, it restores what the lock file lists, with no re-evaluation of dependencies. If it detects a change, it re-evaluates the package graph and updates the lock file to reflect the new closure.
That second behavior is convenient on a developer machine and wrong in CI, where nobody wants the pipeline to change dependencies on the fly.
Locked mode for CI
The documentation provides locked mode for exactly this case.
dotnet restore --locked-mode
In locked mode, restore either installs the exact packages listed in the lock file or fails if you changed the declared dependencies after the lock file was created. You can also set the RestoreLockedMode property in the project file.
| Setting | Lock file missing or stale | Typical use |
|---|---|---|
| Lock file, normal restore | NuGet updates it | Local development |
| Lock file, locked mode | Restore fails | CI and release builds |
| No lock file | Resolves fresh each time | Avoid for shipped apps |
Adoption steps
- Turn on the property in a shared build props file, so every project gets it.
- Restore once, review and commit the lock files.
- Add locked mode to CI and release builds.
- Update dependencies in a branch, regenerate, and review the lock diff in the pull request.
- Pin transitive packages you need to control. Central Package Management has an opt-in transitive pinning feature that promotes a transitive dependency to a top-level one implicitly. The documentation notes downgrades are not allowed, and an attempt raises error NU1109.
Limits
The lock file records versions. It is not a substitute for checking where packages come from, which is a separate topic in NuGet's documentation. If you change settings that affect the dependency tree, such as enabling pruning, the documentation warns the regenerated lock file may list fewer packages than before.
Where Safeguard fits
Safeguard's dependency scanning and SBOM generation can check the packages in a .NET project, including transitive ones, against known vulnerabilities, and a free scan is available at gold.safeguard.sh/scan.
Try it yourself
Open the inventory and look for transitive packages, which are the ones a lock file records and a project file never mentions.
Click through it at your own pace, or open it full screen.
Quick check
What is locked mode?
A restore setting that installs exactly what packages.lock.json lists, or fails if the project dependencies changed after the file was created.
Does the lock file include transitive packages?
Yes. The documentation says it lists all the package dependencies for the project.
Sources
Last reviewed: 19 October 2026.