cargo: command not found, even though rustup is installed

Symptom: rustup works, rustup show lists an installed and active toolchain, but cargo, rustc, rustfmt all fail with “command not found”.

Cause

On macOS with Homebrew, rustup is a keg-only formula. Homebrew only symlinks the rustup binary itself into /opt/homebrew/bin. It does not symlink the proxy shims that rustup normally ships alongside it, cargo, rustc, rustfmt, cargo-clippy, and so on. Those live in the keg:

/opt/homebrew/opt/rustup/bin/

That directory is never added to PATH by Homebrew. So rustup resolves, but every command it would normally dispatch to the active toolchain does not.

You can confirm this with:

$ which rustup
/opt/homebrew/bin/rustup

$ which cargo
cargo not found

$ brew info rustup
==> Caveats
To use rustup, ensure you have "$(brew --prefix rustup)/bin" in your $PATH

The toolchain itself is fine, ~/.rustup/toolchains/<target>/bin/ has real cargo and rustc binaries. The shims in the keg are what normally sit in front of them and pick the active toolchain; without them on PATH, none of that machinery is reachable.

Fix

Add /opt/homebrew/opt/rustup/bin to PATH. opt/rustup is a stable symlink Homebrew maintains to the current Cellar version, so it survives upgrades.

For a plain shell profile:

export PATH="/opt/homebrew/opt/rustup/bin:$PATH"

For a fish config managed with chezmoi, this fits the same pattern as any other per-toolchain PATH fragment (a go.fish.tmpl, dotnet.fish.tmpl, etc.):

if test -d /opt/homebrew/opt/rustup/bin
  path_insert /opt/homebrew/opt/rustup/bin
end

A wrinkle when testing the fix

If you already have an interactive shell open, sourcing the fix in a nested shell may look like a no-op, even though it works fine in a fresh terminal. In this setup, fish’s config caches whether PATH insertion already ran, via a PATHS environment variable set at the end of config.fish. That variable is exported, so it leaks into every child process, including a nested fish -c invocation used to test the fix. The insertion logic sees PATHS already set and skips itself.

This is not a bug in the fix. It is a side effect of testing a shell-startup change from inside a shell that already ran startup once. Verifying it requires clearing that variable for the test invocation:

env -u PATHS fish -c 'cargo --version'

A genuinely new terminal window does not have this problem, since it never inherited the stale PATHS=true.

Takeaway

If a Homebrew-installed CLI tool “works” but the commands it is supposed to expose don’t, check whether the formula is keg-only first. brew info <formula> prints the exact PATH caveat, no guessing required.