.NET Tools

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The C# Reference bibliography lists the reference material consulted while preparing these pages.

A .NET tool is a console application distributed as a NuGet package and run through the dotnet CLI — dotnet-ef, dotnet-format before it joined the SDK, the API-compatibility tool, Ledgerline's own ledgerline command. Tools are installed globally for a user, into a tool path, or locally for a repository through a manifest; since .NET 10 they can also be executed once without installing. This page does all four with .NET SDK 10.0.402 and the Ledgerline.Cli package from Ledgerline's local feed. The overview in the C# Reference is Build and Tooling — Global and Local Tools; distributing command-line tools in general is compared in Packaging and Distribution — dotnet tool; building the package is on Tool and Template Packages.

Three ways to install

Kind Command Installed to Invoked as

Global

dotnet tool install -g <id>

~/.dotnet/tools (shim) and ~/.dotnet/tools/.store

ledgerline (on PATH)

Tool path

dotnet tool install <id> --tool-path <dir>

<dir>

<dir>/ledgerline

Local

dotnet tool install <id> (with a manifest)

the NuGet global-packages folder; recorded in dotnet-tools.json

dotnet ledgerline or dotnet tool run ledgerline

Global

dotnet tool install --global Ledgerline.Cli --add-source ~/ledgerline/artifacts/feed
~/.dotnet/tools/ledgerline check ~/ledgerline/samples/Ledgerline.Report/journal.ledger.csv
dotnet tool list --global
Tools directory '~/.dotnet/tools' is not currently on the PATH environment variable.
If you are using bash, you can add it to your profile by running the following command:

cat << \EOF >> ~/.bash_profile
# Add .NET Core SDK tools
export PATH="$PATH:~/.dotnet/tools"
EOF

You can add it to the current session by running the following command:

export PATH="$PATH:~/.dotnet/tools"

You can invoke the tool using the following command: ledgerline
Tool 'ledgerline.cli' (version '1.0.0') was successfully installed.
OK: 4 lines, every entry balances.
Package Id          Version      Commands
-------------------------------------------
ledgerline.cli      1.0.0        ledgerline

The package ID is matched case-insensitively and reported in lower case; the command name (ledgerline) comes from the package’s ToolCommandName. ~/.dotnet/tools holds a small launcher per command and a hidden .store folder with the package (.store/ledgerline.cli/1.0.0/). A global tool is per user, not per repository: two repositories cannot use two versions of the same global tool.

--tool-path <dir> installs the same way into a chosen folder, which suits CI images and containers that should not depend on a user profile.

Local

A local tool is pinned in a manifest. dotnet new tool-manifest creates one (on SDK 10 at the repository root as dotnet-tools.json; older SDKs used .config/dotnet-tools.json, which is still found). Ledgerline already has one for the API-compatibility tool:

dotnet tool install Ledgerline.Cli --local
cat dotnet-tools.json
dotnet tool list --local
dotnet ledgerline check samples/Ledgerline.Report/journal.ledger.csv
You can invoke the tool from this directory using the following commands: 'dotnet tool run ledgerline' or 'dotnet ledgerline'.
Tool 'ledgerline.cli' (version '1.0.0') was successfully installed. Entry is added to the manifest file ~/ledgerline/dotnet-tools.json.
{
  "version": 1,
  "isRoot": true,
  "tools": {
    "microsoft.dotnet.apicompat.tool": {
      "version": "10.0.402",
      "commands": [
        "apicompat"
      ],
      "rollForward": false
    },
    "ledgerline.cli": {
      "version": "1.0.0",
      "commands": [
        "ledgerline"
      ],
      "rollForward": false
    }
  }
}
Package Id                           Version       Commands        Manifest
--------------------------------------------------------------------------------------------------
microsoft.dotnet.apicompat.tool      10.0.402      apicompat       ~/ledgerline/dotnet-tools.json
ledgerline.cli                       1.0.0         ledgerline      ~/ledgerline/dotnet-tools.json
OK: 4 lines, every entry balances.

The manifest is committed; the tool itself is not. A fresh clone runs dotnet tool restore, which downloads every tool in the manifest:

Run "dotnet tool restore" to make the "ledgerline" command available.

Tool 'microsoft.dotnet.apicompat.tool' (version '10.0.402') was restored. Available commands: apicompat
Tool 'ledgerline.cli' (version '1.0.0') was restored. Available commands: ledgerline

Restore was successful.

The first line is what dotnet ledgerline printed on a machine with empty NuGet and CLI caches. On the original machine, though, emptying only the NuGet global-packages folder (NUGET_PACKAGES) was not enough to reproduce it: the CLI’s tool resolver cache (~/.dotnet/toolResolverCache) still pointed at the old package, and the tool ran. CI should always run dotnet tool restore rather than infer from a missing-tool error. isRoot: true stops the CLI from merging manifests found in parent folders.

Updating, removing, searching

dotnet pack src/Ledgerline.Cli -p:Version=1.1.0 -o ~/feed2
cd ~/ledgerline && dotnet tool update --global Ledgerline.Cli --add-source ~/feed2
cd /tmp && dotnet tool update --global Ledgerline.Cli --add-source ~/feed2
dotnet tool uninstall -g Ledgerline.Cli
  Successfully created package '~/feed2/Ledgerline.Cli.1.1.0.nupkg'.
The --add-source option cannot be combined with package source mapping. To use an additional source, update your NuGet configuration file's source mapping settings. Learn more: https://aka.ms/nuget-package-source-mapping
Tool 'ledgerline.cli' was successfully updated from version '1.0.0' to version '1.1.0'.
Tool 'ledgerline.cli' (version '1.1.0') was successfully uninstalled.

Run inside the Ledgerline folder, the update was refused: its nuget.config uses package source mapping, and an ad hoc --add-source would bypass the mapping. Run from a folder without that configuration it worked. For tools of a repository with source mapping, publish the new version to a mapped feed (or map the extra source) instead (Package Sources and Source Mapping).

dotnet tool search <text> queries nuget.org for packages of type DotnetTool:

Package ID                 Latest Version      Authors                                  Downloads      Verified
---------------------------------------------------------------------------------------------------------------
dotnet-outdated-tool       4.8.1               dotnet-outdated Team & Contributors      7897378
dotnet-tools-outdated      0.8.0               Mojmir Rychly                            1793791

One-shot execution: dotnet tool exec and dnx

NET 10 runs a tool without installing it: dotnet tool exec <id> downloads the package to the cache (in an

interactive terminal it asks for confirmation first; --yes skips that), runs it and leaves nothing on PATH or in a manifest. dnx is a short script shipped next to dotnet that does the same:

dotnet tool exec Ledgerline.Cli --add-source ~/ledgerline/artifacts/feed -- check ~/ledgerline/samples/Ledgerline.Report/journal.ledger.csv
dnx Ledgerline.Cli --yes --add-source ~/ledgerline/artifacts/feed -- balance ~/ledgerline/samples/Ledgerline.Report/journal.ledger.csv
OK: 4 lines, every entry balances.
[
  {
    "Account": "1000-Cash",
    "Debit": 500.00,
    "Credit": 120.00,
    "Balance": 380.00
…

The dnx script on Linux ends with:

SDK_VERSION=$("$DOTNET" --list-sdks | tail -n 1 | cut -d' ' -f1)
…
exec "$DOTNET" exec "$SDK_PATH" dnx "$@"

It always runs the highest installed SDK, ignoring global.json. Inside a repository that pins an SDK, dotnet tool exec respects the pin; dnx does not.

Command Installs Scope

dotnet tool exec / dnx

nothing (package cached)

one run

dotnet tool install -g

global shim

the user

dotnet tool install --tool-path

a folder

that folder

dotnet tool install (local)

manifest entry

the repository

A tool is code that runs with your permissions, and dnx/dotnet tool exec make running an unknown package one command away. Check the package ID, owner and version (prefer reserved-prefix packages, pin @version), and in CI use local tools from a reviewed manifest restored from your own feeds.

RID-specific and Native AOT tools

From .NET 10 a tool package can contain RID-specific, self-contained or Native AOT builds of the tool; the CLI picks the package for the current platform at install time, falling back to a framework-dependent any build when the author published one. Installing needs nothing special — dotnet tool install -g <id> chooses — but the tool then starts without the JIT and needs no installed runtime. Authoring such packages (ToolPackageRuntimeIdentifiers, PublishAot) is covered on Tool and Template Packages.

Common mistakes

  • Global tools for repository builds — versions drift between machines; use a manifest.

  • Skipping dotnet tool restore in CI — the resolver cache can hide a missing restore locally.

  • --add-source in a repository with source mapping — refused; map the source.

  • dnx in a pinned repository — it uses the newest SDK.

  • Forgetting PATH for global tools — the install message prints the line to add.