Skip to content

Migrating off asdf, nvm & direnv

You don’t have to rewrite your world to adopt mise. Almost everything you do with nvm, pyenv, asdf, volta, sdkman and direnv has a direct mise equivalent — usually one line in mise.toml. This page is the translation table, the three gotchas that surprise people, and the one comparison that tells you when not to reach for mise.

You ran……now you run / writeNotes
nvm use 22 / nvm install 22mise use node@22writes nearest mise.toml + installs
pyenv install 3.13 / pyenv local 3.13mise use python@3.13array form for multiple: python = ["3.13", "3.12"]
rbenv install 3.3 / rbenv local 3.3mise use ruby@3.3
volta install node@22mise use -g node@22-g = global (~/.config/mise/config.toml)
sdk install java 21-temmise use java@temurin-21mise registry java to see backends
asdf plugin add X && asdf install X vmise use X@vno plugin add step — see Tools & Backends
asdf global X v / asdf local X vmise use -g X@v / mise use X@vglobal vs nearest config
nvm ls / pyenv versions / asdf listmise lsmise ls -c for what’s active here
nvm ls-remote / asdf list-all Xmise ls-remote X
.nvmrc / .node-version / .python-version / .tool-versions[tools] in mise.tomlidiomatic files are opt-in (gotcha #1)
direnv + .envrc (export FOO=bar)[env] in mise.tomlsee Environments & Secrets
.envrcdotenv .env[env]_.file = ".env"
.envrclayout python (venv)[env]_.python.venv = { path = ".venv", create = true }
Makefile / npm run / just[tasks] + mise runsee The Task Runner

That table is the migration for most repos. Delete the old config, add a mise.toml, commit it.

before — .nvmrc
22
after — mise.toml
[tools]
node = "22" # fuzzy → latest 22.x
Migrating an existing project
Rendering diagram…

1. Idiomatic version files are NOT read by default

Section titled “1. Idiomatic version files are NOT read by default”

mise reads mise.toml and .tool-versions, but it ignores .nvmrc, .node-version, .python-version, .ruby-version and .sdkmanrc until you opt in — per tool. This is deliberate: those files are ambiguous and slow to parse.

Terminal window
# opt a single tool into reading its idiomatic file
mise settings add idiomatic_version_file_enable_tools node
mise settings add idiomatic_version_file_enable_tools python
…or in mise.toml
[settings]
idiomatic_version_file_enable_tools = ["node", "python"]

2. “default packages” files are deprecated

Section titled “2. “default packages” files are deprecated”

asdf/nvm let you list always-install global packages in ~/.default-npm-packages, ~/.default-gems, etc. mise still reads these, but they’re deprecated: they warn from 2026.11.0 and are removed in 2027.11.0. Use a tool postinstall instead — it’s per-tool, lives in config, and runs after every install.

mise.toml
[tools]
# before: ~/.default-npm-packages held `prettier`, `corepack`…
node = { version = "22", postinstall = "corepack enable && npm i -g prettier" }

The postinstall hook receives MISE_TOOL_NAME / MISE_TOOL_VERSION in its environment. More on hooks in Watch, Hooks & Automation.

3. asdf install dirs are not reused — reinstall

Section titled “3. asdf install dirs are not reused — reinstall”

mise does not adopt the tools already sitting in ~/.asdf/installs. The layout and download sources differ (mise uses verified aqua/github downloads, not asdf plugin scripts). Just reinstall — it’s fast and you get checksummed binaries:

Terminal window
mise install # installs everything in the resolved config
mise ls # confirm versions are active

Once you’re happy, you can remove asdf entirely (~/.asdf, the shim line in your rc file, and any asdf Homebrew formula).

asdf is a supported legacy backend, but mise’s default backend in 2026 is aqua, and that’s the one you want. The difference is supply-chain trust:

asdf pluginaqua (mise default)
What runs at installarbitrary bash from a 3rd-party plugin repodeclarative download from a curated registry
Download integritywhatever the script doesverified downloads (checksums)
Windowsmostly unsupportedsupported
Maintenanceoften stale / unmaintainedcentral registry
mise.toml — same tool, better backend
[tools]
# bare name resolves via the registry to aqua where available
ripgrep = "latest"
# or address a backend explicitly
"aqua:BurntSushi/ripgrep" = "latest"

Run mise registry to see which backend a short name resolves to. If a tool only exists as an asdf plugin you can still use asdf:owner/repo, but treat it as a last resort. The full preference order and the lockfile story are in Tools & Backends.

direnv: don’t combine them — port to [env]

Section titled “direnv: don’t combine them — port to [env]”

The official 2026 stance is not to run mise and direnv together. mise is the directory-env manager; layering direnv on top means two tools racing to mutate your environment. Port your .envrc into [env] and delete it.

before — .envrc
export DATABASE_URL=postgres://localhost/dev
export PORT=3000
PATH_add ./bin
after — mise.toml
[env]
DATABASE_URL = "postgres://localhost/dev"
PORT = "3000"
_.path = "./bin" # relative to the config root

The mise direnv interop shim is deprecated — don’t reach for it. Everything direnv did (vars, dotenv, PATH_add, venv layouts) is native in Environments & Secrets, plus secrets and profiles direnv never had.

mise replaces version managers, not your system package manager. Keep Homebrew (or apt/pacman) for system libraries, GUI casks, and OS-level daemons; let mise own your per-project language runtimes and CLI tools.

Keep in HomebrewMove to mise
system libs (openssl, libpq), GUI casksproject runtimes (node, python, go, ruby)
long-lived services you brew services startper-project CLI tools (ripgrep, terraform, kubectl)
mise itself (brew install mise)everything you used to brew install <runtime> for

A common cleanup: brew uninstall node python@3.x once mise pins them per project — no more “which Python is python3?” surprises.

mise is version-based, not hermetic. It puts the right tool versions on your PATH; it does not sandbox your build from the host OS, and it doesn’t run services. Sometimes you want more isolation than that:

NeedReach forWhy not mise
Fully hermetic, hash-locked buildsNix / devboxmise pins versions, not the whole dependency graph
OS-level isolation / a clean Linux userlanddevcontainers / Dockermise shares your host OS and libs
Running databases, queues, daemonsDocker Composemise installs the binary, it does not supervise it
  1. Pin tools: mise use node@22 python@3.13 (or hand-write [tools]).
  2. Port .envrc[env]; port Makefile/npm run[tasks].
  3. Lock it: [settings] lockfile = true, mise lock, commit mise.lock.
  4. Delete .nvmrc / .python-version / .tool-versions / .envrc.
  5. Remove the old tools: asdf, nvm, direnv (and their rc-file shims).
  6. Teammates pull and run mise install — identical setup, one config.

Next: turn this into a team-wide standard in Designing Your Workflow, or grab copy-paste starters from the Cookbook.