📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What are .NET tools, and how do global tools differ from local tools with a tool manifest and dotnet tool restore?

Covers dotnet tool: global vs local vs tool-path installs, the .config/dotnet-tools.json manifest, restore in CI, one-shot execution in .NET 10, and supply-chain safety.

Junior Theory Intermediate ci-cd dotnet-cli dotnet-tool supply-chain tool-manifest

🎯 Key points

  • A .NET tool is a console app packaged as a NuGet package with PackAsTool
  • Global tools (-g) are per user and on PATH; --tool-path installs into a folder you choose; local tools are pinned per repo
  • Local tools are recorded in .config/dotnet-tools.json, created by dotnet new tool-manifest
  • dotnet tool restore makes the pinned versions available on any machine or CI agent
  • The SDK finds the manifest by walking up directories; isRoot stops the search
  • Tools run arbitrary code: pin versions, use trusted feeds, watch for typosquatting
  • In .NET 10, dotnet tool exec (with the dnx shortcut) runs a tool one-off without installing it; good for ad hoc use, but pin a version in CI

⏱ 30-second answer

A .NET tool is a console app shipped as a special NuGet package. Global tools (dotnet tool install -g) are installed per user and land on your PATH. Local tools are pinned per repository in a manifest, .config/dotnet-tools.json, created with dotnet new tool-manifest. Anyone who clones the repo runs dotnet tool restore and gets exactly the same versions. For team and CI work I prefer local tools, because the version lives in source control next to the code that depends on it.

🗣 Full answer (senior)

How I would answer this in an interview

"A .NET tool is a console app packaged as a NuGet package with PackAsTool, like dotnet-ef or dotnet-counters. There are three ways to install one. Global tools, with -g, are per user and on your PATH; --tool-path installs into a folder you choose; and local tools are pinned per repository.

Local tools are recorded in .config/dotnet-tools.json, which I create with dotnet new tool-manifest and commit. Anyone who clones the repo, or any CI agent, runs dotnet tool restore and gets exactly the same versions. The SDK finds the manifest by walking up from the current folder, and isRoot stops that search so a stray manifest higher up can't add tools. In a pipeline, tool restore is one of the first steps, and the pinned dotnet-ef is then what applies or bundles migrations. In .NET 10, dotnet tool exec, or the dnx shortcut, runs a tool one-off without installing it, which is great ad hoc, but in CI I pin a version.

The mistake I see is everyone installing dotnet-ef globally, so versions drift from the EF Core packages and migrations differ between machines. Tools also run arbitrary code with my permissions, so I pin versions, check the package id for typosquatting, and restore through a trusted internal feed with source mapping. I treat the tool manifest like a lock file: reviewed, updated through automation, and restored from a controlled feed."


What a .NET tool is

A .NET tool is a console application packed as a NuGet package with <PackAsTool>true</PackAsTool>. You install it with dotnet tool install, and the SDK puts a launcher in front of it. Common examples are dotnet-ef, dotnet-counters, dotnet-trace, dotnet-dump, dotnet-outdated-tool and csharpier.

Three ways to install

Mode Command Where it lives How you run it Version pinned in repo?
Global dotnet tool install -g dotnet-ef ~/.dotnet/tools (on PATH) dotnet-ef or dotnet ef No
Tool path dotnet tool install dotnet-ef --tool-path ./tools The folder you choose ./tools/dotnet-ef No (unless you script it)
Local dotnet tool install dotnet-ef (with a manifest) NuGet cache, recorded in .config/dotnet-tools.json dotnet ef or dotnet tool run dotnet-ef Yes

Local tools in practice

dotnet new tool-manifest          # creates .config/dotnet-tools.json
dotnet tool install dotnet-ef --version 10.0.0
dotnet tool install csharpier
git add .config/dotnet-tools.json

A teammate or a CI agent then only needs:

dotnet tool restore
dotnet ef migrations list

The SDK searches for the manifest from the current directory upwards, so one manifest at the repo root covers every project.

Other commands worth knowing

  • dotnet tool list and dotnet tool list -g show what is installed.
  • dotnet tool update <id> updates a tool and, for local tools, rewrites the manifest.
  • dotnet tool uninstall <id> removes it.
  • In .NET 10 the SDK added one-shot execution: dotnet tool exec <id>, with the dnx shorthand. It downloads and runs a tool without installing it permanently, which is handy for a one-off but is not a replacement for a pinned manifest in CI.

Production example

On a team where every developer had installed dotnet-ef globally, migrations generated on one laptop differed from another because the tool versions had drifted from the EF Core runtime package. We moved dotnet-ef into a local manifest pinned to the same major version as Microsoft.EntityFrameworkCore, added dotnet tool restore as the first pipeline step, and the drift stopped.

Best practices, performance and security

  • Pin versions in the manifest and update them deliberately, ideally through Dependabot or Renovate pull requests.
  • Tools are code that runs with your permissions. Only install packages from trusted owners, check the package ID carefully (typosquatting is real), and in CI restore through your internal feed or a nuget.config with package source mapping.
  • dotnet tool restore benefits from the NuGet package cache, so cache ~/.nuget/packages between pipeline runs to cut restore time.
  • Do not rely on global tools on build agents: hosted agents are rebuilt often and self-hosted ones drift.

🔍 What the interviewer is checking

Checks whether the candidate knows how teams keep tooling reproducible.

Green flags: explains the manifest and dotnet tool restore; prefers local tools for repo work; mentions version pinning and supply-chain risk.

Red flags: "everyone just installs it globally"; doesn't know where the manifest lives; treats tools as harmless.

📚 Learn in depth

1. What a tool package is

A .NET tool is an ordinary console app packed with <PackAsTool>true</PackAsTool>. The package has a different type from a library package:

 Contoso.ReleaseNotes.1.2.0.nupkg      (package type: DotnetTool)
 +-- Contoso.ReleaseNotes.nuspec
 +-- tools/
     +-- net10.0/
         +-- any/
             +-- DotnetToolSettings.xml   command name -> entry point DLL
             +-- Contoso.ReleaseNotes.dll
             +-- (all its dependencies, bundled)
Library package Tool package
Assemblies in lib/<tfm>/ Assemblies in tools/<tfm>/any/
Added with dotnet add package Added with dotnet tool install
Its dependencies become the consumer's dependencies Its dependencies are bundled inside and isolated
Compiled into your app Runs as a separate process

That is why dotnet add package dotnet-ef doesn't give you the dotnet ef command, and why a tool's dependencies never conflict with your project's.

2. How dotnet <command> is resolved

 dotnet ef migrations add Init
          |
          v
 built-in SDK command (build, test, new ...)?  --yes--> run it
          | no
          v
 walk up from the current directory looking for .config/dotnet-tools.json
 (and dotnet-tools.json in the folder itself); stop at a manifest with isRoot: true
          |
          +-- a manifest lists a tool with command "dotnet-ef"
          |      restored in ~/.nuget/packages?
          |        no  --> error: "Run 'dotnet tool restore'"
          |        yes --> run the PINNED version
          |
          | no manifest entry
          v
 an executable called dotnet-ef on PATH (a global tool)?  --yes--> run it
          | no
          v
 "Could not execute because the specified command or file was not found"

Two consequences: a local tool wins over a global tool with the same command, and the same command can run different versions in different folders.

3. The three install modes compared

Global (-g) Tool path (--tool-path) Local (manifest)
Installed to ~/.dotnet/tools A folder you choose NuGet global packages folder
On PATH Yes (after adding ~/.dotnet/tools) Only if you add it No; run through dotnet <command>
Scope Your user, every folder Whoever uses that folder One repository
Version recorded in source control No Only if your script pins it Yes, in .config/dotnet-tools.json
Restore on a new machine Reinstall by hand Your script dotnet tool restore
Best for Personal utilities you use everywhere Build agents, container images Anything the repo's build, tests or scripts need

4. The manifest in detail

{
  "version": 1,
  "isRoot": true,
  "tools": {
    "dotnet-ef": {
      "version": "10.0.0",
      "commands": ["dotnet-ef"],
      "rollForward": false
    },
    "csharpier": {
      "version": "1.0.2",
      "commands": ["csharpier"],
      "rollForward": false
    }
  }
}
Field Meaning
isRoot Stop searching parent folders, so a stray manifest higher up the disk can't add tools
version (per tool) The exact version dotnet tool restore installs
commands The command names the tool provides; two tools can't claim the same command
rollForward Allow the tool to run on a newer .NET runtime than it targets

Put one manifest at the repo root. dotnet tool install, update and uninstall (without -g) edit it for you.

5. Runtime compatibility

A tool targets a runtime, just like any app:

 tool targets net8.0, machine has only the .NET 10 runtime
   rollForward false --> fails: "You must install or update .NET to run this application"
   rollForward true  --> runs on .NET 10 (usually fine, test it)

Install with --allow-roll-forward (or set rollForward in the manifest) when you know the tool works on newer runtimes. With the .NET 10 SDK, tool authors can also ship RID-specific and Native AOT tool packages, which don't need a matching runtime at all.

6. One-shot execution in .NET 10

dnx dotnet-counters --version 9.0.0 ps        # shorthand
dotnet tool exec dotnet-counters --version 9.0.0 ps
Use it for Avoid it for
Trying a tool once Build and test steps in CI without a pinned version
Running a scaffolder or support script Anything the team runs every day (put it in the manifest)
Diagnosing a machine where you don't want installs

It asks before downloading unless you pass --yes, and it downloads through the same NuGet sources and package source mapping as any other package.

7. CI patterns

 1. setup-dotnet from global.json
 2. restore the NuGet cache (key includes .config/dotnet-tools.json)
 3. dotnet tool restore          <-- first, and let it fail loudly
 4. dotnet restore / build / test
 5. dotnet ef migrations bundle  <-- uses the pinned dotnet-ef
 6. dotnet csharpier check .     <-- uses the pinned formatter

Because local tools restore into the NuGet global packages folder, the same cache, nuget.config, authentication and source mapping apply as for package references.

8. Making your own tool

Property Purpose
PackAsTool Pack as a tool package
ToolCommandName The command users type (defaults to the assembly name)
PackageId, Version, Description Normal package metadata
RollForward=Major Let the tool run on newer runtimes
ToolPackageRuntimeIdentifiers (.NET 10) Ship RID-specific packages, for example Native AOT builds

Pack with dotnet pack, push to a feed, and teams install it like any other tool.

9. Security

A tool runs arbitrary code with your permissions and, in CI, with the pipeline's credentials.

  • Check the package id and owner carefully; typosquatted names are a real attack.
  • Pin versions in the manifest and update them through reviewed pull requests (Dependabot or Renovate understand dotnet-tools.json).
  • Restore through an internal feed with package source mapping.
  • nuget.org packages are repository-signed; dotnet nuget verify checks signatures, and trustedSigners can require them.

10. .NET 11 awareness

One-shot execution and RID-specific tool packages are new in .NET 10. Watch the .NET 11 SDK notes for further changes to tool execution before relying on them in scripts.

11. Try it yourself

  1. In an empty folder run dotnet new tool-manifest and open .config/dotnet-tools.json.
  2. Run dotnet tool install dotnet-counters and see the manifest change.
  3. Install a different version globally with dotnet tool install -g dotnet-counters --version <other>, then run dotnet counters --version inside and outside the folder.
  4. Delete the tool's folder from ~/.nuget/packages, run dotnet counters --version, and read the error. Run dotnet tool restore and try again.
  5. Run dotnet tool list and dotnet tool list -g and compare.
  6. Run dnx dotnet-dump --help and note the confirmation prompt.
  7. Pack the code example's tool project, install it from a local folder with dotnet tool install Contoso.ReleaseNotes --add-source ./artifacts, and run dotnet release-notes.

💻 Code

# GitHub Actions: restore pinned tools before using them
- uses: actions/setup-dotnet@v4
  with:
    global-json-file: global.json

- name: Cache NuGet packages
  uses: actions/cache@v4
  with:
    path: ~/.nuget/packages
    key: nuget-${{ runner.os }}-${{ hashFiles('**/packages.lock.json', '.config/dotnet-tools.json') }}

- name: Restore local tools
  run: dotnet tool restore

- name: Check formatting with a pinned formatter
  run: dotnet csharpier check .
<!-- Making your own tool -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <PackAsTool>true</PackAsTool>
    <ToolCommandName>release-notes</ToolCommandName>
    <PackageId>Contoso.ReleaseNotes</PackageId>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

⚠ Common mistakes

  • Mistake: installing dotnet-ef globally on build agents. The agent's version drifts from the EF Core packages in the project, so migrations and bundles differ between machines or fail. Instead: pin dotnet-ef in the repo's tool manifest to the same major version as Microsoft.EntityFrameworkCore, and run dotnet tool restore in CI.

  • Mistake: forgetting dotnet tool restore in CI. The job fails with "Run 'dotnet tool restore' to make the tool available" or "command not found", often several steps later. Instead: make dotnet tool restore an early step and let it fail the job loudly, without || true around tool calls.

  • Mistake: a manifest without isRoot: true in a monorepo. A manifest in a parent folder adds tools nobody expected, or supplies a different version. Instead: set isRoot: true in the repo's manifest.

  • Mistake: installing tools from unknown publishers or mistyped ids. A tool runs arbitrary code with your credentials, including pipeline secrets. Instead: check the owner and id, pin versions, and restore through a trusted feed with package source mapping.

  • Mistake: using dnx or dotnet tool exec without a version in a pipeline. The build depends on whatever version is latest that day. Instead: pass an explicit --version, or put the tool in the manifest.

  • Mistake: treating --tool-path as reproducible on its own. It installs into a folder you control, but the version is only pinned if your script pins it. Instead: pass --version in the install script, or use a manifest.

  • Mistake: trying to add a tool with dotnet add package. A tool package isn't a library, so the reference fails or does nothing useful. Instead: use dotnet tool install.

  • Mistake: assuming a global tool is on PATH. On Linux and macOS, ~/.dotnet/tools isn't always added to PATH, and some tools also need DOTNET_ROOT. Instead: add ~/.dotnet/tools to PATH in your shell profile, or use local tools through dotnet <command>.

  • Mistake: ignoring runtime compatibility. A tool built for net8.0 won't start on a machine with only the .NET 10 runtime. Instead: install the needed runtime, or allow roll forward with --allow-roll-forward (or rollForward in the manifest) after checking the tool works.

🏛 Senior insight: trade-offs

Local tools turn developer tooling into a versioned dependency, which is what you want in a team. A senior engineer treats the tool manifest like the lock file: reviewed, updated through automation (Dependabot or Renovate pull requests), and restored from a controlled feed.

For security and reliability they go further:

  • Route tool restores through the same internal feed with upstream sources and package source mapping as library packages, so tools can't come from an unexpected source.
  • Rely on NuGet's signature checks: nuget.org packages are repository-signed, dotnet nuget verify checks a package's signature, and trustedSigners in nuget.config can require signatures from specific publishers.
  • Keep the feed's audit logs and the pipeline logs, so you can say which tool versions ran in which build.

In regulated Australian environments (for example, aligning with the ACSC Essential Eight on application control), that evidence matters. The trade-off is a little friction: people must run dotnet tool restore after cloning, so put it in the README, in a bootstrap script and as the first pipeline step.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate uses dotnet-ef as the example tool. Why: dotnet-ef is the most common local tool; moves into how it is used safely in pipelines.
    How to answer

    I generate migrations locally with dotnet ef migrations add, then make deployment a separate, reviewed step. The pipeline produces an artefact: either an idempotent SQL script (dotnet ef migrations script --idempotent) for a DBA to review, or a migration bundle (dotnet ef migrations bundle), a self-contained executable that applies pending migrations. That artefact runs once, before the new app version starts, with a dedicated deployment identity. I avoid Database.Migrate() at startup because several instances can race, the app needs DDL permissions it shouldn't have, and a failed migration turns into a crash loop.

  2. When: Candidate says CI runs dotnet tool restore. Why: Checks where tool restore and pinned formatters fit in a pipeline.
    How to answer

    The pipeline restores three times, builds in Debug, runs dotnet format in a way that silently fixes files instead of failing, and publishes from a different build than the one it tested. I'd use global.json for the SDK, restore once with --locked-mode, build once in Release with --no-restore, then test and publish with --no-build. I'd run dotnet format --verify-no-changes so formatting drift fails the build, collect test results and coverage, and cache the NuGet folder.

  3. When: Candidate mentions restoring tools from an internal feed. Why: Tool restore uses NuGet feeds and credentials, so feed failures show up here too.
    How to answer

    First I read the exact error. NU1301 means a source couldn't be reached, 401 or 403 means authentication, and NU1101 means the package isn't on any configured source. It works on my machine because I have cached packages and stored credentials, while CI starts clean. Then I check which nuget.config CI actually uses, since config files are merged from the repo upwards. I make the repo's config explicit with <clear />, and I authenticate CI the supported way, for example the Azure Artifacts credential provider or a token from the pipeline's secret store, never a committed password. I also add package source mapping and lock files so restores are reproducible and protected against dependency confusion.

↩ Usually asked after

More in .NET CLI

Last updated 1 Nov 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me