Originally published on kuryzhev.cloud
The scenario
A shared repo needs a single mise toolchain file because three version managers disagree about what "the right version" means. The frontend wants Node 26 from .nvmrc, the scripts want Python 3.15 from .python-version, and the infrastructure folder wants Terraform 1.17 from .terraform-version. Each manager has its own install command, shell hook and startup cost. A new teammate gets three setup sections in the README, and CI gets a fourth.
Short answer: install mise, run mise use --pin for each tool to create one mise.toml at the repo root, and commit it. Then remove the old managers from your shell profile. This fits teams that need the same Node, Python, Terraform or Go versions on laptops and in CI. It is a poor fit if your build already runs entirely in pinned container images, or if one person owns a single-language repo, where the existing manager is fine.
The rest of this guide is a migration runbook. Do it on one repository first, and keep the old managers installed until the verification step passes.
Prerequisites
You need a Linux or macOS shell (bash or zsh; fish also works), curl or Homebrew, and a repo where you know the current versions. Before touching anything, record what the old tools resolve to. That record is your rollback baseline.
# Capture the current state before migrating
node -v; python --version; terraform version | head -1
cat .nvmrc .python-version .terraform-version 2>/dev/null
npm ls -g --depth=0 # global npm packages you may need to reinstall
pyenv versions # note any virtualenvs tied to pyenv
Check this baseline against the current releases: Node.js 26 LTS (26.13.0), Python 3.15 (3.15.2), Terraform 1.17 (1.17.1) and OpenTofu 1.13 (1.13.5). If the repo still pins an end-of-life runtime, decide now whether to upgrade as a separate change. Mixing a manager migration with a major version bump makes any breakage hard to attribute.
Watch out: pyenv virtualenvs and nvm global packages do not move to mise. Plan to recreate virtual environments (python -m venv .venv) and reinstall global CLIs. Prefer project-local dependencies where you can.
Step 1: Install mise and activate it in your shell
The documented installer is a one-liner, and Homebrew and most distro package managers also carry mise. Read the script before piping it to a shell if your policy requires it.
curl https://mise.run | sh # installs to ~/.local/bin/mise
# or: brew install mise
# bash; use "zsh" in ~/.zshrc if that is your shell
echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
exec bash
mise doctor # reports PATH, activation and config problems
mise activate hooks your prompt so the right tool versions are on PATH when you enter a directory. For non-interactive contexts such as IDE launchers, cron or some CI steps, mise also supports shims (mise activate --shims) or explicit mise exec. The mise docs describe the trade-offs between the two.
Leave nvm, pyenv and tfenv in place for now. Mise activation placed later in your profile normally takes precedence, but confirm that with which node in the verification step instead of assuming it.
Step 2: Write one mise toolchain file
Let mise create the file for you, and pin exact versions so every machine resolves the same binary. Without --pin, mise use node@26 records the loose prefix 26, which tracks newer 26.x patch releases as they are installed.
cd your-repo
mise use --pin node@26.13.0 python@3.15.2 terraform@1.17.1
# creates or updates ./mise.toml and installs the tools
The resulting file should look like this. You can also hand-write it.
# mise.toml (commit this file)
[tools]
node = "26.13.0" # Node.js 26 LTS
python = "3.15.2"
terraform = "1.17.1" # swap for: opentofu = "1.13.5" if you run OpenTofu
[env]
# Non-secret defaults only. Never commit credentials here.
# CI-only variables such as TF_IN_AUTOMATION belong in the CI job, not here.
Two documented behaviors matter here. First, mise python normally installs precompiled builds by default, so you skip the long compile that pyenv does. If a brand-new patch release is not yet available as a prebuilt binary, check the mise Python docs for the fallback. Second, mise asks you to trust config files that can set environment variables or run code. Run mise trust in a repo you have reviewed.
Watch out: the [env] section is plain text in git. Use it for harmless defaults and keep secrets in your secrets manager or a git-ignored file.
Step 3: Handle legacy version files and retire the old managers
Teams rarely migrate in one pull request. Mise can read "idiomatic" version files such as .nvmrc, .node-version and .python-version, so colleagues who have not switched yet keep working. Which tools it reads these for is a setting, and the default has changed across releases, so check the current docs. A typical opt-in looks like this:
mise settings add idiomatic_version_file_enable_tools node
mise settings add idiomatic_version_file_enable_tools python
mise settings ls | grep idiomatic # confirm what is enabled
Once the whole team is on mise, treat mise.toml as the single source of truth and delete the old files. Otherwise two files can silently disagree. Then remove the old managers:
# 1. Delete the nvm, pyenv and tfenv init lines from ~/.bashrc / ~/.zshrc
# 2. Only after the "Verify and test" checks pass, remove the old installs:
rm -rf ~/.nvm ~/.pyenv ~/.tfenv
git rm .nvmrc .python-version .terraform-version
If you want an easy way back, postpone the rm -rf for a week. The old directories are harmless once their shell hooks are gone.
Step 4: Make CI use the same file
The payoff comes when CI reads the same mise.toml as laptops. In GitHub Actions, the maintained jdx/mise-action installs mise and runs mise install from the repo config. Check its README for the current major tag instead of copying one from an old blog post. For any other CI system, or a Dockerfile, the same commands work:
curl https://mise.run | sh
export PATH="$HOME/.local/bin:$PATH"
export TF_IN_AUTOMATION=1 # CI-only: Terraform runs non-interactively
mise trust # only for a repo whose mise.toml you have reviewed
mise install # installs everything in mise.toml
mise exec -- terraform init -backend=false # validate needs providers and modules
mise exec -- terraform validate # runs with the pinned toolchain, no shell hook needed
mise exec (or mise x) avoids depending on prompt hooks, which non-interactive jobs do not have. Cache the mise data directory (typically ~/.local/share/mise) between runs to avoid downloading tools every time. Mise also supports a lockfile (mise.lock) that records resolved versions and checksums. It is worth enabling if you need stricter supply-chain guarantees. Confirm the setting name and maturity in the docs for your installed version.
Cost to be aware of: mise downloads binaries from upstream sources at install time. In locked-down networks, mirror them or bake the toolchain into a base image.
Verify and test
Verification means proving that the shell resolves tools through mise and that the versions match your baseline. Start in the repo directory, in a fresh shell.
mise doctor # should end without warnings about PATH or activation
mise ls --current # tool, version, and the source file it came from
which node python terraform # paths should sit under ~/.local/share/mise/
node -v; python --version; terraform version | head -1
cd /tmp && node -v # outside the repo: falls back to global or system version
Expected shape: mise ls --current lists three rows whose "Requested" column matches mise.toml and whose source points at your repo path. The versions printed must equal the baseline you captured earlier, or the intentional upgrade you chose. If which node still points to ~/.nvm/..., an old hook is loading after mise in your profile. Move the mise line to the end.
Then run the repo's real work: npm ci && npm test, a fresh python -m venv .venv install, and terraform init -backend=false && terraform validate. Finally, open a pull request and confirm that CI resolves the same versions. Printing mise ls --current as a CI step makes drift visible in logs.
When not to do this: avoid it if your security policy forbids third-party binary downloads without an internal mirror. If Terraform's licence terms are a concern for you, pin opentofu instead, or stay with distro packages.
Where this leaves you
After the migration, a new teammate runs mise install and gets the exact Node, Python and Terraform versions the repo expects, and CI reads the same file. Your shell profile loses three init hooks, and version bumps become one reviewable diff to mise.toml. Keep the rollback window short, recreate virtualenvs rather than copying them, and read the mise documentation for settings that vary by release. For more runbooks on everyday DevOps tooling, browse kuryzhev.cloud.
Top comments (0)