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.
The big before/after table
Section titled “The big before/after table”| You ran… | …now you run / write | Notes |
|---|---|---|
nvm use 22 / nvm install 22 | mise use node@22 | writes nearest mise.toml + installs |
pyenv install 3.13 / pyenv local 3.13 | mise use python@3.13 | array form for multiple: python = ["3.13", "3.12"] |
rbenv install 3.3 / rbenv local 3.3 | mise use ruby@3.3 | |
volta install node@22 | mise use -g node@22 | -g = global (~/.config/mise/config.toml) |
sdk install java 21-tem | mise use java@temurin-21 | mise registry java to see backends |
asdf plugin add X && asdf install X v | mise use X@v | no plugin add step — see Tools & Backends |
asdf global X v / asdf local X v | mise use -g X@v / mise use X@v | global vs nearest config |
nvm ls / pyenv versions / asdf list | mise ls | mise ls -c for what’s active here |
nvm ls-remote / asdf list-all X | mise ls-remote X | |
.nvmrc / .node-version / .python-version / .tool-versions | [tools] in mise.toml | idiomatic files are opt-in (gotcha #1) |
direnv + .envrc (export FOO=bar) | [env] in mise.toml | see Environments & Secrets |
.envrc → dotenv .env | [env] → _.file = ".env" | |
.envrc → layout python (venv) | [env] → _.python.venv = { path = ".venv", create = true } | |
Makefile / npm run / just | [tasks] + mise run | see The Task Runner |
That table is the migration for most repos. Delete the old config, add a
mise.toml, commit it.
22[tools]node = "22" # fuzzy → latest 22.x3.13.0[tools]python = "3.13" # fuzzy; pin exact with mise use --pin
[env]_.python.venv = { path = ".venv", create = true } # replaces `pyenv-virtualenv` / `layout python`3.3.0[tools]ruby = "3.3"The migration path
Section titled “The migration path”
flowchart TD
start["existing project<br/>.nvmrc / .python-version / .tool-versions / .envrc"] --> tools["mise use <tool>@<ver><br/>writes [tools] in mise.toml"]
tools --> env{".envrc?"}
env -->|yes| port["port exports → [env]<br/>dotenv → _.file"]
env -->|no| lock
port --> lock["mise lock + commit mise.lock"]
lock --> clean["delete .nvmrc / .envrc / .tool-versions"]
clean --> done["mise install reproduces it everywhere"]
The three gotchas
Section titled “The three gotchas”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.
# opt a single tool into reading its idiomatic filemise settings add idiomatic_version_file_enable_tools nodemise settings add idiomatic_version_file_enable_tools python[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.
[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:
mise install # installs everything in the resolved configmise ls # confirm versions are activeOnce you’re happy, you can remove asdf entirely (~/.asdf, the shim line in your
rc file, and any asdf Homebrew formula).
asdf → aqua: why the backend changed
Section titled “asdf → aqua: why the backend changed”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 plugin | aqua (mise default) | |
|---|---|---|
| What runs at install | arbitrary bash from a 3rd-party plugin repo | declarative download from a curated registry |
| Download integrity | whatever the script does | verified downloads (checksums) |
| Windows | mostly unsupported | supported |
| Maintenance | often stale / unmaintained | central registry |
[tools]# bare name resolves via the registry to aqua where availableripgrep = "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.
export DATABASE_URL=postgres://localhost/devexport PORT=3000PATH_add ./bin[env]DATABASE_URL = "postgres://localhost/dev"PORT = "3000"_.path = "./bin" # relative to the config rootdotenv .env[env]_.file = ".env" # dotenv / JSON / YAML / TOMLlayout python python3.13[tools]python = "3.13"
[env]_.python.venv = { path = ".venv", create = true }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.
Homebrew stays — it’s complementary
Section titled “Homebrew stays — it’s complementary”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 Homebrew | Move to mise |
|---|---|
system libs (openssl, libpq), GUI casks | project runtimes (node, python, go, ruby) |
long-lived services you brew services start | per-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.
When NOT to reach for mise
Section titled “When NOT to reach for mise”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:
| Need | Reach for | Why not mise |
|---|---|---|
| Fully hermetic, hash-locked builds | Nix / devbox | mise pins versions, not the whole dependency graph |
| OS-level isolation / a clean Linux userland | devcontainers / Docker | mise shares your host OS and libs |
| Running databases, queues, daemons | Docker Compose | mise installs the binary, it does not supervise it |
A clean cut-over
Section titled “A clean cut-over”- Pin tools:
mise use node@22 python@3.13(or hand-write[tools]). - Port
.envrc→[env]; portMakefile/npm run→[tasks]. - Lock it:
[settings] lockfile = true,mise lock, commitmise.lock. - Delete
.nvmrc/.python-version/.tool-versions/.envrc. - Remove the old tools:
asdf,nvm,direnv(and their rc-file shims). - 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.