Manage Node, Python and Go versions per project with mise

Pin Node, Python and Go versions per project with mise: install, shell activation, mise use, mise.toml vs .tool-versions, and moving off asdf or nvm.

how-to8 steps

Updated

in short

Install mise, add mise activate to your shell rc file, then run mise use node@22 python@3.12 go@1.23 in a project. mise writes the versions to mise.toml and switches them when you cd into the directory.

mise installs language runtimes and other dev tools, records which versions a project needs in a config file, and puts the right ones on PATH per directory. One tool covers what nvm, pyenv and a Go version manager do separately, and it reads asdf's .tool-versions files.

The same mise.toml can also hold environment variables and project tasks. This guide focuses on runtime versions and touches on the rest briefly.

Tools used

Before you start

  • macOS, Linux or Windows; this guide uses bash, zsh or fish
  • curl for the install script, or Homebrew, apt, dnf or another package manager
  • Remove or plan to remove nvm, asdf or pyenv lines from your shell rc file (step 8)

Steps

  1. Step 1: Install mise

    The official script installs a single binary to ~/.local/bin/mise. Homebrew and most Linux package managers also ship mise; the project notes that the Homebrew build is separate from its release binaries.

    sh
    curl https://mise.run | sh
    ~/.local/bin/mise --version
    sh
    brew install mise
  2. Step 2: Activate mise in your shell

    Activation adds a hook that updates PATH and environment variables at each prompt, so the versions follow you between directories. Put it in the rc file your interactive shell reads, not ~/.bash_profile or ~/.zprofile. Use the full path ~/.local/bin/mise if that directory is not on PATH yet.

    Open a new shell and run mise doctor; it should report activated: yes. With Homebrew and fish, mise activates itself and you can skip this step.

    zsh
    # ~/.zshrc
    eval "$(mise activate zsh)"
    bash
    # ~/.bashrc
    eval "$(mise activate bash)"
    fish
    # ~/.config/fish/config.fish
    mise activate fish | source
    sh
    mise doctor
  3. Step 3: Pin versions for a project with mise use

    From the project root, mise use installs each tool and writes the request to mise.toml. A partial version such as 22 selects the newest installed or available 22.x release; it is not an exact pin. Add --pin to write the full resolved version instead.

    For Go 1.20 and older, where 1.20 is itself a release, use go@prefix:1.20 to get the latest 1.20.x.

    sh
    cd ~/code/my-app
    mise use node@22 python@3.12 go@1.23
    toml
    # mise.toml (written by the command above)
    [tools]
    go = "1.23"
    node = "22"
    python = "3.12"
    sh
    mise ls --current
    node --version
  4. Step 4: Install a project's tools after cloning

    Commit mise.toml. On another machine, or for a teammate, mise install installs everything the config declares without changing it. Review config files from other people first: tasks, hooks and some [env] directives can run code, and mise trust marks a reviewed file as trusted.

    sh
    git clone https://github.com/example/my-app && cd my-app
    mise install
    mise exec -- node --version
  5. Step 5: Choose between mise.toml and .tool-versions

    mise.toml is the native format and the one to use unless teammates still run asdf. It supports [env], [tasks] and tool options. .tool-versions is asdf's format; mise reads it and can write it with --path, using the asdf names (nodejs, golang). If both files exist in a directory, mise.toml wins for the tools it declares.

    mise can also read per-language files such as .nvmrc, .node-version and .python-version, but that is off by default. Enable it per tool.

    sh
    # keep a shared .tool-versions for asdf users
    mise use --path .tool-versions --pin node@22
    sh
    # read .nvmrc / .node-version and .python-version
    mise settings add idiomatic_version_file_enable_tools node
    mise settings add idiomatic_version_file_enable_tools python
  6. Step 6: Set global defaults

    mise use -g writes to ~/.config/mise/config.toml. Those versions apply anywhere a project config does not override them, for example in your home directory or in repos without a mise.toml.

    sh
    mise use -g node@lts python@3.12 go@latest
  7. Step 7: Add environment variables and tasks

    The [env] table sets variables while you are in the project; _.file loads a .env file, and for Python _.python.venv creates and activates a virtualenv. [tasks] defines commands you run with mise run, which get the project's tools and environment.

    toml
    # mise.toml
    [env]
    NODE_ENV = "development"
    _.file = ".env"
    _.python.venv = { path = ".venv", create = true }
    
    [tasks.test]
    description = "Run the test suite"
    run = "npm test"
    sh
    mise run test
    mise tasks ls
  8. Step 8: Migrate from asdf or nvm

    From asdf: mise reads existing .tool-versions files, so run mise install in each project and check it with mise exec -- node --version. mise does not reuse asdf's install directory. Once that works, delete the asdf lines and its shims directory from PATH in your rc file.

    From nvm: enable .nvmrc support (step 5), set a default with mise use -g node@lts, and remove the NVM_DIR export and the line that sources nvm.sh. Globally installed npm packages do not carry over; reinstall the ones you need.

    Open a new shell and confirm with mise doctor and type -a node.

Troubleshooting

  • Versions do not change when you cd: mise activate is in a profile file, not your rc file. Move it to ~/.bashrc or ~/.zshrc.
  • The wrong node runs: compare mise ls --current node, mise which node and type -a node. Leftover nvm or asdf lines usually come first on PATH.
  • A script cannot find a tool right after mise install: installing does not change the running shell. Use mise exec -- <command>.
  • A project config seems ignored: it may be untrusted. mise doctor lists untrusted files; mise trust marks a reviewed one as trusted.
  • .nvmrc or .python-version is ignored: idiomatic version files are off by default. Enable them with mise settings add idiomatic_version_file_enable_tools <tool>.
  • mise use go@1.20 installs 1.20.0 instead of the latest patch: use go@prefix:1.20 for Go 1.20 and older.

asdf commands in mise

  • asdf install nodejs 22.11.0 becomes mise install node@22.11.0.
  • asdf set nodejs 22.11.0 (asdf 0.16+) becomes mise use node@22.11.0, which also installs it.
  • asdf list all nodejs becomes mise ls-remote node.
  • asdf current becomes mise ls --current.
  • asdf which node becomes mise which node; asdf reshim becomes mise reshim.
  • mise set writes environment variables, not tool versions; use mise use for versions.

Tips

  • Put personal overrides in mise.local.toml and add it to .git/info/exclude so it stays out of commits.
  • mise use --dry-run node@22 shows which file would change before it writes anything.
  • mise exec -- <command> and mise run work without activation, which suits CI and cron.

Also worth a look

  • fnmNode-only version manager that reads .nvmrc and .node-version; fewer moving parts if you only need Node.
  • direnvLoads per-directory environment variables from .envrc; pair it with a version manager if you only need env vars.

FAQ

What is the difference between `mise use` and `mise install`?
`mise use` installs a tool and writes the version to a config file. `mise install` only installs what the config already declares, or a version you name, without editing any file.
Do I have to activate mise?
No. `mise exec -- <command>` and `mise run <task>` load the project's tools and environment for one command. Activation is for interactive shells, so plain `node` or `python` picks the right version.
Can teammates on asdf share the project?
Yes, if you keep versions in `.tool-versions` and avoid mise-only syntax in it. mise reads the same file, and `mise use --path .tool-versions` writes to it.

related