Your .NET application restores its dependencies successfully. Visual Studio builds the solution, tests pass, and everything seems to work.

But where are those packages coming from? Would another developer—or your CI pipeline—use the same sources?

A repository-level nuget.config lets you declare which package feeds your application should use instead of relying on whatever is configured on individual machines. This works with traditional .NET Framework projects; you don’t need to convert them to SDK-style or upgrade their target framework.

We’ll discover the configuration NuGet is using, make an explicit decision about sources, and verify that the tools actually honor it. We’ll use RecordSift, a .NET Framework 4.8 application, as an example.

A successful restore doesn’t prove your sources are controlled

Imagine two developers cloning the same repository. One has only nuget.org configured. The other also has a company feed and an old local package directory. Both run a restore successfully.

That does not prove they restored from the same configured sources. NuGet combines settings from machine-, user-, and directory-level configuration files. More specific settings take precedence; entries in collections such as package sources are inherited unless cleared.

Our goal isn’t merely to make restore succeed. It’s to make the source policy understandable, intentional, and testable—much like the coding conventions we established in our EditorConfig article.

Step 1: Discover the configuration you’re inheriting

Before changing anything, identify the configuration files NuGet considers. The .NET CLI commands in this section run on Windows, macOS, and Linux.

From your solution directory, run:

dotnet nuget config paths

Illustrative output:

Windows

C:\Users\<username>\projects\myapp\nuget.config

Linux

/home/<username>/projects/myapp/nuget.config

macOS

/Users/<username>/projects/myapp/nuget.config

The command requires .NET SDK 8.0.2xx or later.

Inspect every file it lists to identify where an unexpected setting originated, such as a user-level configuration, a parent directory, or your repository.

Next, inspect the effective package sources:

dotnet nuget list source

Illustrative output:

Registered Sources:
  1.  nuget.org [Enabled]
      https://api.nuget.org/v3/index.json
  2.  CompanyFeed [Enabled]
      https://packages.example.com/nuget/v3/index.json

This example shows two enabled feeds; CompanyFeed is hypothetical. If you expected only nuget.org, investigate which configuration introduced the additional source. The source listing does not identify its originating file, so refer to the configuration paths above.

See Microsoft’s NuGet configuration documentation for the configuration hierarchy. You can also inspect package sources in Visual Studio under Tools → Options → NuGet Package Manager → Package Sources. Changing user settings, however, is not necessarily a repository change.

Compare inherited configuration with one specific file

If your repository contains nuget.config, you can inspect its settings independently:

dotnet nuget list source --configfile ./nuget.config

Illustrative output:

Registered Sources:
  1.  nuget.org [Enabled]
      https://api.nuget.org/v3/index.json

Without --configfile, NuGet evaluates the applicable configuration hierarchy. With it, NuGet uses only the specified file. Don’t use --configfile when testing whether normal configuration inheritance works.

Does capitalization matter?

We use lowercase nuget.config throughout our examples. NuGet recognizes several filename spellings during automatic discovery on case-sensitive file systems; its commit-pinned implementation explicitly prefers lowercase.

When using --configfile, supply the filename’s actual spelling. The RecordSift modernization case study now uses lowercase nuget.config consistently.

Step 2: Declare an explicit package-source policy

RecordSift’s original application baseline already declared nuget.org as its package source:

<packageSources>
  <clear />
  <add key="nuget.org"
       value="https://api.nuget.org/v3/index.json" />
</packageSources>

The <add> element names an approved feed. The <clear /> element removes inherited entries from this collection, so an unexpected machine- or user-level feed doesn’t silently join the list.

A second collection can still disable the source

NuGet tracks disabled sources separately. Suppose a developer’s user-level configuration contains:

<disabledPackageSources>
  <add key="nuget.org" value="true" />
</disabledPackageSources>

Even though RecordSift’s packageSources section explicitly includes nuget.org, this inherited entry can disable it. Clearing packageSources doesn’t clear disabledPackageSources.

If the approved policy is that the repository’s declared nuget.org source should be enabled, add:

<disabledPackageSources>
  <clear />
</disabledPackageSources>

Respect organizational source restrictions

This is a deliberate policy decision, not a rule to copy into every project. Clearing inherited feeds can remove a company-mandated internal source; clearing disabled-source entries can also override a company decision to disable public feeds.

If your organization requires packages to come through an internal repository, configure that policy instead. nuget.config by itself isn’t a security boundary: enforced network restrictions may also be required.

Keep vulnerability auditing separate

RecordSift also declares an audit source:

<auditSources>
  <clear />
  <add key="nuget.org-audit"
       value="https://api.nuget.org/v3/index.json" />
</auditSources>

packageSources controls where packages can be obtained; auditSources controls where NuGet gets information about known vulnerabilities. They can point to different services.

NuGet Audit supports traditional packages.config projects from NuGet 6.10, and separate auditSources from NuGet 6.12. If auditSources is absent or empty, NuGet falls back to package sources for advisory information. Explicitly specifying both prevents that fallback from becoming an accidental policy if package feeds change later. See Microsoft’s NuGet Audit documentation.

Step 3: Verify inheritance with positive and negative controls

We investigated source inheritance on a disposable Windows GitHub Actions runner, using parent and child configuration files. This diagram shows the arrangement using our standardized lowercase filename:

NuGetTest/
├── nuget.config
└── Application/
    └── nuget.config

The diagram uses our standardized lowercase filename. The four results below came from the original EXP-044 research run. Subsequent stage CI verified the disabled-source behavior and package-restore controls using lowercase nuget.config.

The parent configuration introduced an extra feed and disabled nuget.org:

<configuration>
  <packageSources>
    <add key="test-feed" value="C:\NuGetTestFeed" />
  </packageSources>
  <disabledPackageSources>
    <add key="nuget.org" value="true" />
  </disabledPackageSources>
</configuration>

The child configuration—our positive control—declared nuget.org and cleared both inherited collections:

<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org"
         value="https://api.nuget.org/v3/index.json" />
  </packageSources>
  <disabledPackageSources>
    <clear />
  </disabledPackageSources>
</configuration>

From the child directory, we ran:

dotnet nuget list source

We changed one configuration setting at a time and repeated the command.

These are the observed results from the original EXP-044 research run:

Test Actual nuget.org status Other inherited source
Both collections cleared Enabled test-feed absent
Remove packageSources clear Enabled test-feed present
Remove entire disabledPackageSources section Disabled test-feed absent
Restore both clear elements Enabled test-feed absent

With both collections cleared, the command returned:

Registered Sources:
  1.  nuget.org [Enabled]
      https://api.nuget.org/v3/index.json

Removing the package-source <clear /> exposed not only our test-feed, but also the runner’s inherited Microsoft Visual Studio Offline Packages source:

Registered Sources:
  1.  nuget.org [Enabled]
      https://api.nuget.org/v3/index.json
  2.  test-feed [Enabled]
      C:\NuGetTestFeed
  3.  Microsoft Visual Studio Offline Packages [Enabled]
      C:\Program Files (x86)\Microsoft SDKs\NuGetPackages\

Removing the disabled-source section instead produced:

Registered Sources:
  1.  nuget.org [Disabled]
      https://api.nuget.org/v3/index.json

Restoring both clear elements returned nuget.org to [Enabled]. All four comparisons produced the expected results. Your machine may list other inherited sources when the package-source clearing rule is removed.

Verified environment

Component Verified version
GitHub Actions runner windows-2022
.NET SDK 10.0.403
NuGet CLI 7.9.0.83
PowerShell 7.6.9

The experiment ran on Windows, although the source-listing command itself is cross-platform.

The original outputs are retained in EXP-044 and its successful research run. The post-rename stage run also passed with the lowercase file.

Step 4: Test an actual restore—not just the source listing

dotnet nuget list source verifies effective configuration, but it doesn’t prove that package acquisition succeeds or fails because of that configuration.

We added a second experiment using a disposable packages.config with NUnit 3.14.0, a dependency already used by RecordSift. The experiment used the standalone nuget.exe CLI on Windows; it was not run on macOS or Linux:

<packages>
  <package id="NUnit" version="3.14.0" targetFramework="net48" />
</packages>

From the disposable Application directory, the verification script ran the same command twice: once with the child’s disabledPackageSources clearing rule removed, and once with it present. In each run, $destination was a different, initially empty temporary package directory.

Actual command (PowerShell syntax):

nuget restore .\packages.config -NonInteractive -PackagesDirectory $destination -DirectDownload -NoHttpCache -Verbosity detailed

There was no -ConfigFile or -Source override. The direct-download and no-HTTP-cache options limited cache interference.

The two controls produced these observed results:

Restore condition Exit code NUnit installed?
Inherited nuget.org disabled; no child disabled-source clear 1 No
Same setup, child clears disabled sources 0 Yes

The failing restore log explicitly reports:

Restoring NuGet package NUnit.3.14.0.
Unable to find version '3.14.0' of package 'NUnit'.

The successful control instead shows NuGet downloading nunit.3.14.0.nupkg from nuget.org, installing it, and recording nuget.org under Feeds used. The paired source listings, the package-not-found message, and the successful retry with the clearing rule establish the observed difference. Full logs, including NuGet’s cleanup diagnostics, are retained with the workflow artifact.

The same positive and negative controls passed after the lowercase rename. The successful post-rename stage workflow retains both complete restore logs and results.

Verify the application solution

We separately ran an ordinary RecordSift solution restore without -ConfigFile, using a fresh package directory.

Actual stage command (PowerShell syntax):

nuget restore .\RecordSift.sln -NonInteractive -PackagesDirectory $packagesPath -Verbosity detailed

The stage script assigned $packagesPath a fresh temporary directory.

Observed results:

Configuration Effective source Restore result
Original two-section configuration nuget.org enabled Exit 0
Three-section modernization-stage configuration nuget.org enabled Exit 0

Both solution restores succeeded without an explicit configuration-file override.

These solution restores confirm that both checked-in configurations work on the tested clean Windows runners. They do not exercise an inherited disabled-source setting. Only the controlled NUnit restore demonstrates the effect of the fix on package acquisition.

Step 5: Preserve a reviewable before-and-after case study

The restore experiments justified one behavioral change: add a disabledPackageSources clearing section while retaining the existing package and audit sources.

We made a separate convention cleanup at the same time: standardize the repository’s root filename as lowercase nuget.config and update its references. That rename was not a conclusion of the source-inheritance experiments; it was a repository consistency decision.

The complete case-study configuration is:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
  <disabledPackageSources>
    <clear />
  </disabledPackageSources>
  <auditSources>
    <clear />
    <add key="nuget.org-audit" value="https://api.nuget.org/v3/index.json" />
  </auditSources>
</configuration>

For a reproducible comparison, see the original application baseline commit and the merged nuget.config at a fixed commit. The accompanying commit-pinned PowerShell verification script checks the configuration and its exact Git filename casing, and runs the same checks locally and in Windows CI.

PR #268 merged into the modernization/case-study branch at commit d91557d, not main.

No dependencies, project formats, target frameworks, or application code were changed to demonstrate the improvement.

What if you use private package feeds?

Applications using multiple feeds need additional care. An internal package identifier can also exist on a public feed, creating a dependency confusion risk if source eligibility isn’t sufficiently constrained.

NuGet’s Package Source Mapping can restrict package identifiers or patterns to eligible sources. Consider it when your approved policy involves multiple feeds.

Don’t commit feed passwords or access tokens into nuget.config. Use appropriate credential providers or secure CI authentication instead.

What this configuration doesn’t guarantee

Controlling feeds is one piece of predictable package restoration, not a guarantee of fully reproducible or secure builds. Dependency versions, transitive resolution, caches, restore tools, command-line overrides, and security information all matter.

In traditional packages.config projects, NuGet doesn’t support using NoWarn or TreatWarningsAsErrors to control audit-warning severity in the same way as PackageReference projects. RecordSift instead examines NuGet restore output for NU1900 through NU1905. This detects those warnings, but a clean warning log alone doesn’t prove that an advisory service was contacted.

Frequently asked questions

Should every .NET repository contain a nuget.config?

We recommend source-controlled NuGet configuration for independently maintained applications when package-source policy should be explicit. A repository already governed by an intentional monorepo-level configuration may not need a duplicate file.

Does nuget.config require SDK-style projects?

No. It also works with traditional .NET Framework projects. Individual commands and settings still require compatible NuGet tooling.

Does nuget.config replace packages.config?

No. packages.config identifies packages and versions for traditional projects; nuget.config controls NuGet behavior, including sources.

Does it make restores reproducible?

Not on its own. It makes an important part of the environment explicit and testable, but doesn’t control every input to dependency resolution.

Final thoughts

Modernization doesn’t always begin with a framework upgrade.

For RecordSift, we investigated an inherited setting that could override the intent of an existing repository configuration. We reproduced the issue, confirmed the effective source listings, used an NUnit restore to verify package acquisition with and without the clearing rule, and preserved a small, reviewable change. We also adopted a consistent lowercase filename as a separate convention cleanup.

The XML change was simple. The verification made it meaningful.

Understand the defaults. Take control of the configuration. Verify the behavior.