lilpacy.infoJA
Killing the Global-Tools Problem with Nix: Three Concepts asdf Users Should Know

Killing the Global-Tools Problem with Nix: Three Concepts asdf Users Should Know


TL;DR

When you switch Node versions with asdf/nvm, the tools you installed with npm i -g disappear. This is a structural problem: “the install location of the tools is inside the runtime’s version directory.” pip has the same issue. Nix prevents this from ever happening by sealing each tool together with its dependencies into /nix/store. The only concepts you need to learn are Store, Profile, and nixpkgs.

The Problem: Why Did prettier Disappear?

Anyone who uses asdf/nvm/nodenv has seen this at least once.

asdf global nodejs 20.0.0
npm i -g prettier
prettier --version  # works

asdf global nodejs 22.0.0
prettier --version  # command not found

prettier hasn’t been deleted. It just became invisible.

npm i -g writes to ~/.asdf/installs/nodejs/20.0.0/lib/node_modules/. When you switch to version 22, the symlinks on PATH redirect to 22.0.0/, so prettier inside 20.0.0/ falls off PATH.

~/.asdf/installs/nodejs/
  20.0.0/
    lib/node_modules/
      prettier/  ← it's here
  22.0.0/
    lib/node_modules/
      (empty)    ← but PATH points here

This isn’t an npm-specific problem. pip has the exact same structure. Switch Python versions with asdf and pip install-ed tools like black or ruff vanish too.

The essence of the problem is that the install location of the CLI tools sits inside the language runtime’s version directory. Switching versions means swapping out the whole box, and the tools inside the box go with it.

flowchart LR
    subgraph asdf["asdf (version manager)"]
        N20["Node 20<br/>prettier ✓"]
        N22["Node 22<br/>(empty)"]
    end
    Switch["asdf global nodejs 22"]
    N20 -->|switch| Switch
    Switch --> N22

The fix is obvious: take the tools out of the box.

Existing Approaches and Their Limits

There are several existing approaches to this problem.

ApproachHow it worksLimit
~/.default-npm-packagesAuto npm i -g when Node is installedReinstalls per version. No pip equivalent
npx / pipxAvoid global installs entirelyConstant downloads get annoying for frequently-used tools
HomebrewInstall independently of language runtimesSome packages are missing or outdated
miseasdf-compatible, with improved tool managementLimited support for tools not in the registry
VoltaManages global tools independently of Node versionsNode-only. Doesn’t cover Python-based tools

Each one is a partial fix, but few of them satisfy the requirement: manage CLI tools in fully isolated form, regardless of language.

That’s where Nix comes in.

The Three Nix Concepts

Nix has a sprawling conceptual system, but for the purpose of “decoupling global CLI tools from language runtimes,” you only need three of them. You can ignore the Nix language, flake.nix, and NixOS for now.

1. Nix Store — the warehouse that seals everything

The heart of Nix is a special directory called /nix/store. Every package is stored at a unique path with a hash.

/nix/store/abc123...-prettier-3.2.0/bin/prettier
/nix/store/def456...-nodejs-22.1.0/bin/node

The crucial part: the node that this prettier depends on is hardcoded to the def456...-nodejs-22.1.0 above. Wherever asdf points ~/.asdf/shims/node, it has zero effect on the prettier inside the Nix Store.

That’s why people say Nix’s isolation is “complete.” Where traditional npm i -g writes to $(asdf where nodejs)/lib/node_modules/, Nix isn’t even on that map.

flowchart TB
    subgraph store["/nix/store"]
        P["prettier-3.2.0<br/>(bundles: nodejs-22.1.0)"]
        B["black-24.4.0<br/>(bundles: python-3.12.0)"]
        R["ruff-0.5.0"]
        J["jq-1.7"]
    end
    subgraph asdf["asdf (separate layer)"]
        AN["Node 20 / 22"]
        AP["Python 3.11 / 3.12"]
    end
    store -.-|"do not interfere"| asdf

prettier contains its own Node. black contains its own Python. So no matter what you switch on the asdf side, the tools in the Store are unaffected.

2. Profile — the “what I want available right now” list

Putting things in the Store doesn’t put them on PATH. That’s where Profiles come in.

~/.nix-profile/bin/ is a collection of symlinks to the binaries inside /nix/store, and it sits on PATH. nix profile install adds links; nix profile remove deletes them.

~/.nix-profile/bin/prettier → /nix/store/abc123...-prettier-3.2.0/bin/prettier
~/.nix-profile/bin/black    → /nix/store/def456...-black-24.4.0/bin/black
~/.nix-profile/bin/ruff     → /nix/store/ghi789...-ruff-0.5.0/bin/ruff

If the Store is the warehouse, the Profile is the showcase. The warehouse holds tons of items; the showcase only lines up “what I want to use right now.”

3. nixpkgs — a giant, language-agnostic registry

Where do packages come from? There’s a single Git repository called nixpkgs containing over 100,000 package definitions.

It’s the equivalent of the npm registry or PyPI, with one decisive difference: it’s not tied to a single language. Node-based prettier, Python-based black, Rust-based ripgrep, Go-based gh — all installable from the same registry with the same command.

nix profile install nixpkgs#prettier   # Node-based
nix profile install nixpkgs#black      # Python-based
nix profile install nixpkgs#ripgrep    # Rust-based
nix profile install nixpkgs#gh         # Go-based

The easiest way to search for packages is search.nixos.org. Search by name and filter by the Packages tab and you’ll find it instantly. You can also search from the CLI (nix search nixpkgs prettier), but it surfaces lots of deprecation warnings, irrelevant Vim plugin hits, and so on, so in practice you end up needing filters like nix search nixpkgs prettier 2>/dev/null | grep -E '^\* .*\.prettier '. Confirm package names on the web, install from the CLI — that split causes the least friction. Most major CLI tools are present.

How the Three Concepts Relate

Putting the three concepts together looks like this.

flowchart LR
    NP["nixpkgs<br/>(registry)"]
    NS["/nix/store<br/>(warehouse)"]
    PR["~/.nix-profile/bin/<br/>(showcase)"]
    U["user's PATH"]
    NP -->|"nix profile install"| NS
    NS -->|"symlinks"| PR
    PR -->|"is on PATH"| U
  1. Pull a package definition from nixpkgs, build (or fetch a prebuilt) binary, and store it in the Store
  2. The Profile manages symlinks into the Store
  3. Profile’s bin/ is on PATH, so the user can run the tools directly

Practice: Splitting Responsibilities Between asdf and Nix

Now that the concepts are clear, let’s get into operations.

Installation

Use the Determinate Systems installer. The setup is easier than the official installer, and flakes and the new nix command are enabled out of the box.

curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install

Restart your shell, and once nix --version works you’re done.

Day-to-Day Commands

You only need to remember six commands.

# Find a tool (search.nixos.org is easier; CLI version below)
nix search nixpkgs prettier 2>/dev/null

# Install
nix profile install nixpkgs#prettier

# See what's installed
nix profile list

# Upgrade everything to the latest
nix profile upgrade --all

# Run once (equivalent of npx / pipx run)
nix run nixpkgs#cowsay -- "hello"

# Clean up
nix profile wipe-history && nix store gc

The Splitting Rule

The operational rule is simple.

asdf  → development runtimes (Node, Python)
         switched per project via .tool-versions

Nix   → CLI tools (prettier, black, ruff, awscli, jq, ...)
         not tied to language versions

These two operate as fully separate layers. As long as you don’t put the same tool name in both asdf and Nix, you won’t get PATH collisions. You don’t need to throw away asdf, and you don’t need to migrate everything to Nix.

What You Don’t Need to Know

Finally, let’s call out what’s unnecessary for this use case. People say “Nix has a steep learning curve,” but that’s because they try to learn all of Nix.

ConceptWhy it’s not needed
Nix languageYou don’t write it unless you’re authoring packages
Flakes / flake.nixNeeded for project-level environment management. Not needed for global tool management
DerivationThe build recipe for a package. Consumers don’t need to know the internals
home-managerDeclarative dotfiles management. A separate layer
NixOSNix as a Linux distribution. Irrelevant for macOS users
overlay / overridePackage customization. No use case here

Store, Profile, nixpkgs. Knowing just these three will fully cover global CLI tool management. If you ever want to go deeper, flakes and devShells are waiting — but only when you actually need them.

That’s all.