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.
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
| Parameter | Type | Description |
|---|---|---|
$install / update / uninstall | verb | Manage one tool. Local by default (a manifest is created in the current folder if none is found); -g for global. |
$-g, --globaloptional | flag= false (local) | Install for the user into ~/.dotnet/tools, which must be on PATH. |
$--tool-pathoptional | path | Install into a specific folder, e.g. inside a container image. |
$--versionoptional | string= latest stable | Exact version or range to install. |
$restoreoptional | verb | Install every tool listed in the manifest at its pinned version. |
$run <command>optional | verb | Run a local tool; dotnet <command> is the shorter form. |
$exec / dnxoptional | verb | Download 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
| Kind | Install | Lives in | Use for |
|---|---|---|---|
| Local | dotnet tool install dotnet-ef --version 10.0.13 | .config/dotnet-tools.json in the repository, packages in the NuGet cache | Anything the build or the team depends on: dotnet-ef, code generators, formatters |
| Global | dotnet tool install -g dotnet-counters | ~/.dotnet/tools, on PATH | Personal diagnostics you use across repositories |
| Tool path | --tool-path /tools | The folder you name | Diagnostic images and CI images |
| One-off | dnx <package> or dotnet tool exec | NuGet cache only | Trying 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
RollForwardtoMajor; the error names the missing framework. - Global tools need
~/.dotnet/toolson PATH. In Docker build stages the shell profile is not read, so add it withENV PATHor use--tool-path. dnxruns 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-pathinstall.
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
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.
{
"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.
<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.
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