The Aspire CLI, Revisited

October 01, 2026
Series: Aspire
The Aspire CLI, Revisited
Barret Blake

Barret Blake, Architect

Back in November of 2025, I wrote a post walking through the Aspire CLI. At the time it was a fairly new addition to the Aspire toolkit, and my summary was that it was “still early days, and there are some quirks and gaps.”

That was a year and several releases ago. A lot of those gaps have been filled in. Some of the quirks got fixed. And a few of the things I told you in that post are now flat-out wrong, including one command that doesn’t exist anymore.

So let’s go back through it. We’ll cover what’s new, but more importantly, we’ll cover what changed out from under that original post. Because, if you bookmarked it, some of it will lead you astray now.

The short version of where things landed: the CLI has stopped being a convenience wrapper around dotnet run and has become the primary interface to Aspire, including a fair bit that used to require opening a browser.

Installing it changed

The install instructions in the old post still work, but they’re no longer the best option.

As of 13.3, the Aspire CLI ships as a NativeAOT .NET global tool. It’s always been a dotnet tool, but 13.3 takes advantage of .NET 10’s support for distributing NativeAOT-compiled tools. That means instant startup with no JIT warmup, and no managed runtime dependency in the tool package itself. If you have .NET 10 installed:

dotnet tool install -g Aspire.Cli

Note the missing --prerelease flag. My old post had it. You don’t need it anymore.

13.5 added two more options. There’s now an npm package:

npm install -g @microsoft/aspire-cli

And a Nix flake:

nix profile add github:microsoft/aspire#aspire-cli

A nice touch here: the update notifier detects npm-installed versions and prints the matching npm command rather than overwriting the managed binary out from under you. Somebody thought about that one.

The curl and PowerShell installers from the old post still work fine. As a rough guide for which to pick: the installer script or dotnet tool on a dev machine, npm if your CI runner already has Node on it, and Nix if you’re already living that life.

The CLI bundle

This one is new since the last post and it’s the piece I’d most want a team to know about.

The CLI bundle is a copy of the Aspire CLI that an AppHost can resolve for itself. The point is that dotnet run and aspire run behave identically, and everybody on the team — plus CI — ends up on a consistent CLI version without anyone needing a global install.

In 13.5, new C# AppHosts created from the templates opt in automatically by setting AspireUseCliBundle=true. When the bundle is enabled and the CLI is 13.5.0 or later, dotnet run acquires the bundle on the fly through dnx and delegates to aspire run. That’s a behavior change from earlier releases, but it isn’t one you have to configure.

Existing projects aren’t affected unless you opt in. The SDK-level default is still false. Set AspireUseCliBundle=true in your AppHost to adopt it, or pin the CLI through a local tool manifest if you want a reproducible dnx invocation.

What I appreciate is that the state is explicit rather than silent. You get ASPIRE009 as an error when the bundle can’t be resolved, ASPIRE010 as a warning when a project opts out, and ASPIRE011 when dnx isn’t available. You can also force the DNX invocation path with AspireCliInvocationMode=Dnx.

If you’ve ever spent an afternoon on a “works on my machine” problem that turned out to be two developers running different CLI versions, this is the fix for that.

Things I told you that aren’t true anymore

Here’s the part where I correct myself.

aspire exec is gone

I documented aspire exec in the old post as a preview command for running a command against a specific resource. It was removed in 13.4, along with its AppHost backchannel and feature flag.

If you have scripts or workflows calling it, they need to move to aspire resource when the resource exposes a command. More on that below.

aspire init does less on purpose

This is the biggest correction. In the old post I said aspire init “will analyze the current project and add the appropriate Aspire AppHost functionality.”

As of 13.3, that’s not what it does. aspire init now drops a minimal AppHost skeleton and an aspire.config.json file into your repository, and installs the aspireify agent skill alongside them. It does not wire up resources, projects, or integrations. You complete that wiring by invoking the aspireify skill from your AI agent of choice.

The release notes flag this explicitly as a behavior change, and it’s worth sitting with for a second. Whether this is an improvement depends entirely on whether you use coding agents. If you do, it’s a reasonable division of labor — the CLI does the deterministic part, the agent does the judgment part. If you don’t, aspire init went from doing most of the job to doing considerably less of it.

aspire publish and aspire deploy grew up

In the old post, publish was in preview, and about deploy I wrote: “Currently, no built-in deployment functionality exists. You must take advantage of the DeployingCallbackAnnotation functionality to create your own deployment process.”

Both commands went generally available in 13.4, and deploy now has built-in support for Docker Compose, Kubernetes, AKS, and Azure Container Apps. There’s also an aspire destroy that tears down what deploy created.

That’s a large enough change that it got its own post, so I won’t re-cover it here.

aspire ps lost some flags

13.3 started filtering hidden resources by default — proxies, helper containers, migrations and the like — across aspire ps, aspire describe, and friends, with --include-hidden to see them anyway.

Then 13.5 narrowed aspire ps to AppHost-level summaries and dropped both --resources and --include-hidden from it. So if you were using either of those, that work moves to aspire describe, which is where detailed resource data lives now. --include-hidden still exists on aspire describe and on the aspire resource subcommand — it’s only gone from aspire ps.

The appHostPath thing

In the old post I complained, at some length, about aspire run latching onto the first AppHost it found and then refusing to let go — and I called it a terrible design. It’s still terrible, and still “broken” IMO.

13.3 added AppHost path guardrails, where the CLI’s global config now validates AppHost paths to prevent pointing at the wrong project. It still doesn’t work the way I would want it to. It will first look at the folder you run it from. If it detects an AppHost, it will run that one. Otherwise, it still defaults to the one that got set in the aspire config.

I still believe it should run the sub-folders of the directory you run it from and run the AppHost it finds there (or if there’s multiple, show you a list and let you select one). Until it does that, I will still call it “broken”.

aspire doctor is the command I didn’t know I wanted

This one barely gets mentioned anywhere, and it’s probably the most practically useful thing added in this window.

aspire doctor runs a set of checks against your machine and tells you what’s wrong with it.

One thing to know before you run it: what you get back depends on how current your CLI is, and on where you run it from. Mine is a version behind — you’ll see it telling me so at the bottom — and a few things the current docs describe don’t show up in my output. There’s no Aspire section reporting the CLI version, no operating system line under Environment, and no Aspire CLI Installations table after the summary. There’s also no AppHost section, but that one’s expected, since I ran this from a directory with no AppHost in it.

With that caveat, here’s what it looks like on mine:

C:\projects>aspire doctor

Aspire Environment Check
========================

.NET SDK
  ✅ .NET 10.0.302 installed (x64)

Container Runtime
  ✅ Docker v29.8.0: running (auto-detected (default)) ← active

Environment
  ✅ HTTPS development certificate is trusted
  ⚠️ HTTPS development certificate has an older version (v5)
       Run 'aspire certs clean' to remove all certificates, then run 'aspire certs trust' to create and trust a new one.
       See: https://aka.ms/aspire-prerequisites#dev-certs

Summary: 3 passed, 1 warnings, 0 failed
For detailed prerequisites: https://aka.ms/aspire-prerequisites

A new version of Aspire is available: 13.5.4
To update, run: aspire update

That dev certificate warning is a good example of why the command is worth running. It’s not failing anything today, but it’s the kind of thing that turns into an afternoon of confusion three weeks from now, and it comes with the exact two commands needed to fix it.

The checks are grouped by category, and which ones you see depends on your machine and where you run it from:

  • Aspire reports the CLI version and flags when a newer one is available.
  • AppHost reports the AppHost Aspire SDK version, but only when you run it from a directory containing an AppHost project. That’s the one I’d most want in a team setting — CLI and SDK version mismatches are easier to spot here than in a restore error.
  • .NET SDK and Container Runtime check the obvious prerequisites. The container check handles both Docker and Podman, marks which one is active with ← active, and tells you why it’s active — explicit configuration, auto-detected default, or the only runtime running.
  • Environment covers the OS and the HTTPS development certificate, plus the JavaScript toolchain if you’re running a TypeScript AppHost.
  • Development tools only appears when VS Code is detected, and warns if the Aspire extension isn’t installed.

The Aspire CLI Installations table I mentioned above is the one I’m most curious about. Per the docs it lists every CLI binary on the machine — the active one, peer installs, shadowed binaries, and dotnet tool store installs, with the path, version, channel, and PATH status of each. “Why is this thing behaving like an older version than the one I just installed” is a genuinely annoying afternoon, and that table is the answer to it. Update your CLI first if you want to see it.

Two things make this useful beyond a one-time setup check. First, it takes --format Json for automation, and it returns exit code 1 when any check fails and 0 when everything passes, with warnings still counting as a pass. That makes it a reasonable first step in a CI job or a dev container build — fail the run early if the machine can’t build the thing.

It also pairs with something I mentioned in the deployment post: Aspire 13.4 and 13.5 packages aren’t binary-compatible with each other, and mixing them fails at runtime. aspire doctor won’t catch the package mismatch itself, but it will catch the CLI-and-SDK half of that problem.

One footnote: aspire doctor replaces the old hidden aspire setup command. That still works for backward compatibility, but it’s out of the help output.

Pipeline summaries and running this in CI

The old post covered aspire do in about two sentences, mostly because there wasn’t much to say. There is now.

At the end of aspire do, aspire publish, aspire deploy, and aspire destroy, the CLI prints a summary of the pipeline execution. Which steps ran, how long each took, and whether they succeeded, laid out as a timeline:

✅ 5/5 steps succeeded • Total time: 0.43s

      0.73ms  ✓ validate-compute-environments
      0.21ms  ✓   before-start
       0.41s  ✓ pipeline-execution
       0.41s  ✓ custom-deploy-prereq
      0.33ms  ✓   deploy

In a CI log, that’s the difference between “the deploy failed somewhere” and “the deploy failed at custom-deploy-prereq.” It’s a small feature that saves a disproportionate amount of time.

A few other things landed alongside it:

  • aspire do --list-steps prints the steps that would run for do, publish, deploy, or destroy, without executing any of them.
  • check-container-runtime is a built-in pipeline step that fails fast when no container runtime is available, instead of letting you find out during the build.
  • Independent steps continue on sibling failure, so one broken step no longer blocks unrelated work in the same run.
  • Non-interactive mode was substantially improved in 13.3, and a lot of commands picked up new options specifically to support --non-interactive.

That last one is what makes the whole thing viable in a pipeline. Here’s the preview-environment workflow I promised in the deployment post — deploy when a PR opens, tear it down when the PR closes:

name: PR Preview

on:
  pull_request:
    types: [opened, synchronize, closed]

jobs:
  deploy:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'
      - run: dotnet tool install -g Aspire.Cli
      # Authenticate to your deployment target here.
      - run: aspire deploy --project . --non-interactive

  destroy:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'
      - run: dotnet tool install -g Aspire.Cli
      # Same authentication as the deploy job.
      - run: aspire destroy --project . --non-interactive

You’ll have noticed the gap in the middle of both jobs, and it’s deliberate. A fresh ubuntu-latest runner has no credentials for anything, so as written those two commands will get as far as needing to talk to your deployment target and stop.

What fills that gap depends on where you’re deploying. For Azure targets it’s an azure/login step, using either OIDC federated credentials or a service principal stored in repository secrets. For a Kubernetes cluster it’s whatever gets a kubeconfig onto the runner. If you’re pushing images, you’ll need a registry login too. Whatever it is, it has to come before aspire deploy — and the destroy job needs the same credentials as the deploy job, because it’s reaching for the same resources on the way back out.

Resource commands are scriptable now

If you read my custom resource commands post from January, this is the follow-up.

As of 13.4, resource command inputs are passed as named options rather than positional arguments. Optional inputs are easy to skip, and you can supply values in any order:

aspire resource cache seed-data --dataset small --force

Running aspire resource <resource> --help also shows you the commands available for that specific resource, which is a lot friendlier than remembering what you wired up six months ago.

The built-in set-parameter and delete-parameter commands use named options too, which means scripts can manage parameter resources without hitting an interactive prompt:

aspire resource mydb-password set-parameter --value "MyStr0ngP@ssword"
aspire resource mydb-password delete-parameter --delete-from-user-secrets true

One gotcha: boolean options require explicit true or false values. --force style flags won’t cut it on those built-in commands.

A quick note on agent skills

aspire agent init now installs the Aspire skills bundle from the microsoft/aspire-skills repository. There are six workflow skills in it — aspire, aspire-init, aspire-orchestration, aspire-monitoring, aspire-deployment, and aspireify:

aspire agent init --skill-locations standard --skills aspire,aspire-init,aspire-orchestration,aspire-monitoring,aspire-deployment,aspireify

The design decision worth noting is the split. Rather than one broad playbook loaded for every Aspire task, an agent pulls focused instructions for first-run setup, local lifecycle management, telemetry investigation, deployment, or AppHost wiring, depending on what it’s actually doing.

There’s a whole post in that, and it’s coming later in this series. For now it’s enough to know the command exists and that it’s how aspire init expects you to finish the job.

Where that leaves us

A year ago the Aspire CLI was a convenient way to scaffold a project and run it. Today it installs four different ways, resolves its own version per-repo, diagnoses your machine, deploys to four targets, tears them back down, and reports on its own pipeline.

It also quietly removed a command I’d documented and changed what another one does, which is the cost of writing about a tool moving this fast.

Next time we’ll look at the piece I skipped here: querying logs and telemetry from the terminal, and running the Aspire dashboard without an AppHost at all.