dotnet tool

Install, pin and restore .NET tools per repository, run one-off tools with dnx, and package an internal CLI as a tool of your own.

Signature
dotnet tool install|update|uninstall <PACKAGE_ID> [-g|--global | --tool-path <dir> | --local] [--version <v>]; dotnet tool restore | list | run <command>; dotnet tool exec <PACKAGE_ID> (alias dnx)
  • Added in .NET .NET Core 2.1 (global tools); local tools since .NET Core 3.0

Parameters

ParameterTypeDescription
$install / update / uninstallverbManage one tool. Local by default (a manifest is created in the current folder if none is found); -g for global.
$-g, --globaloptionalflag= false (local)Install for the user into ~/.dotnet/tools, which must be on PATH.
$--tool-pathoptionalpathInstall into a specific folder, e.g. inside a container image.
$--versionoptionalstring= latest stableExact version or range to install.
$restoreoptionalverbInstall every tool listed in the manifest at its pinned version.
$run <command>optionalverbRun a local tool; dotnet <command> is the shorter form.
$exec / dnxoptionalverbDownload and run a tool once without installing it.

Return value

process exit code0 on success. Non-zero when the package is not a tool, the version is missing, or a tool command conflicts with one already installed. A local install with no manifest creates .config/dotnet-tools.json in the current folder instead of failing.

Errors

Fails when a command name is already taken by another tool, or when the tool's target runtime is not installed and its RollForward setting does not allow a newer one.

Changelog

  • PHP .NET Core 3.0Local tools with a .config/dotnet-tools.json manifest and dotnet tool restore.
  • PHP .NET 10dotnet tool exec and its dnx shortcut run a tool without installing it.

A .NET tool is a console app packed as a NuGet package with PackAsTool. dotnet tool installs and runs them. The decision is where a tool lives, because that decides whether every developer and every CI run gets the same version.

Local, global or tool path

KindInstallLives inUse for
Localdotnet tool install dotnet-ef --version 10.0.13.config/dotnet-tools.json in the repository, packages in the NuGet cacheAnything the build or the team depends on: dotnet-ef, code generators, formatters
Globaldotnet tool install -g dotnet-counters~/.dotnet/tools, on PATHPersonal diagnostics you use across repositories
Tool path--tool-path /toolsThe folder you nameDiagnostic images and CI images
One-offdnx <package> or dotnet tool execNuGet cache onlyTrying a tool or running it once in a script

Choose local tools for anything in the build. The manifest is committed, dotnet tool restore installs exactly those versions, and updating a tool is a reviewed one-line diff. A global tool is whatever version each machine happened to install, which is how "the migration works on my laptop" starts.

Manifests

dotnet new tool-manifest creates .config/dotnet-tools.json. The CLI searches the current folder and its parents for a manifest, so run commands from inside the repository. When it finds none, a local dotnet tool install creates a manifest in the current folder rather than failing, so a stray manifest can appear in a project subfolder; run dotnet new tool-manifest at the repository root first, or pass --create-manifest-if-needed false to make the install fail instead. A local tool runs as dotnet ef or dotnet tool run dotnet-ef. In CI, dotnet tool restore is one step before anything uses a tool; it fails if a pinned version is missing from the feeds, which is the signal you want.

Runtime and PATH failures

  • Tools are framework-dependent console apps. A tool built for an older runtime does not start on a machine that only has .NET 10 unless the tool sets RollForward to Major; the error names the missing framework.
  • Global tools need ~/.dotnet/tools on PATH. In Docker build stages the shell profile is not read, so add it with ENV PATH or use --tool-path.
  • dnx runs the newest version of a tool unless you ask for a specific one (dnx dotnet-counters@<version>). Fine at a developer terminal; in a pipeline use a pinned local tool. Production images have no SDK and should not pull unpinned packages from a public feed, so put diagnostics into a dedicated image with a pinned --tool-path install.

Writing your own

Internal CLIs (an order re-pricing tool, an invoice exporter, a schema linter) ship well as tools: set PackAsTool, ToolCommandName and RollForward, dotnet pack, push to the internal feed, and teams pin it in their manifest. Return non-zero exit codes for failures so scripts can rely on them.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Pin dotnet-ef 10.0.13 in .config/dotnet-tools.json so every developer and CI run generates migrations with the same tool
  • Ship an internal invoice-total CLI to all teams as a versioned .NET tool on the company NuGet feed
  • Try dotnet-counters once on a developer machine with dnx dotnet-counters@<version>, and bake a pinned --tool-path copy into a diagnostics sidecar image for production

Frequently asked questions

Should dotnet-ef be a global or a local tool?

Local. Pin it in .config/dotnet-tools.json at the same version as EF Core and run dotnet tool restore; a global install drifts from the packages in each repository.

Why is my global tool command not found?

~/.dotnet/tools is not on PATH in that shell, which is common in Docker build stages and CI. Add it to PATH or install with --tool-path into a folder you call directly.

What is dnx?

A shortcut for dotnet tool exec in the .NET 10 SDK: it downloads a tool package and runs it once without adding it to a manifest or the global tools folder.

More examples

The committed manifest

<code>.config/dotnet-tools.json</code>. <code>isRoot</code> stops the CLI from merging manifests found in parent folders. Updating a tool is <code>dotnet tool update dotnet-ef --version 10.0.14</code>, which rewrites this file for review in a pull request.

Example · json
{
  "version": 1,
  "isRoot": true,
  "tools": {
    "dotnet-ef": {
      "version": "10.0.13",
      "commands": ["dotnet-ef"],
      "rollForward": false
    },
    "contoso.invoice-total": {
      "version": "1.3.0",
      "commands": ["invoice-total"],
      "rollForward": false
    }
  }
}

Packing the internal tool

<code>dotnet pack -c Release</code> produces <code>Contoso.InvoiceTotal.1.3.0.nupkg</code>; push it to the internal feed and pin it in each repository's manifest. Without <code>RollForward</code>, a tool built for net10.0 refuses to start on a machine with only the next major runtime.

Example · xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

    <PackAsTool>true</PackAsTool>
    <ToolCommandName>invoice-total</ToolCommandName>
    <PackageId>Contoso.InvoiceTotal</PackageId>
    <Version>1.3.0</Version>
    <!-- Keep working on machines that only have a newer major runtime. -->
    <RollForward>Major</RollForward>
    <PackageOutputPath>../../artifacts/packages</PackageOutputPath>
  </PropertyGroup>
</Project>

Tools in CI and in a diagnostics image

<code>dotnet tool restore</code> installs exactly the versions from the manifest, so CI runs the same <code>dotnet-ef</code> as every developer. Passing <code>--no-build</code> and the same configuration reuses the solution build instead of compiling the API a second time.

Example · yaml
name: migrations-check
on: [pull_request]
jobs:
  ef:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          global-json-file: global.json
      - run: dotnet tool restore
      - run: dotnet build Shop.slnx -c Release
      - run: >
          dotnet ef migrations has-pending-model-changes
          --project src/Orders.Data --startup-project src/Orders.Api
          --configuration Release --no-build

Discussion

  • Be the first to comment on this lesson.