An Introduction to mise, the Multi-Language Version Manager
See how mise manages Node and Python versions per project, handles existing version files, locks tools, sets environment variables, and runs consistent builds locally and in CI.
mise is a single command-line tool that installs and switches versions of Node, Python, Ruby and other runtimes per directory, reading them from a mise.toml file at the project root.
If your repo has a Vite frontend and a Python script that generates content before the build, you are probably running nvm and pyenv side by side and hoping every teammate’s shell agrees with CI. Usually one of them doesn’t.
For a side-by-side look at mise, nvm, pyenv, rustup and SDKMAN!, see 5 Version Managers Every Developer Should Know. This article skips the feature list. Instead it takes one repo and follows it through cd, existing .nvmrc files, a teammate’s first clone, CI, a two-runtime build task and env vars.
Key Takeaways
- With shell activation, entering a directory that has a
mise.tomlputs that project’s pinned runtimes on PATH, and leaving it restores the previous versions. - mise ignores
.nvmrcand.python-versionunless you turn them on per tool with theidiomatic_version_file_enable_toolssetting. mise runloads the project’s tools and env itself and installs missing tools by default, so it works in CI without shell activation.jdx/mise-action@v4installs mise and the tools inmise.toml. If amise.lockis committed, it installs with--locked.- The
[env]table is plain text that gets committed. It is for configuration, not secrets.
The Project: A Vite Frontend With a Python Build Step
The example repo, docs-preview, has two runtimes, one config file and one workflow:
docs-preview/
├── content/*.md
├── scripts/build_content.py
├── src/
├── package.json # vite in devDependencies
├── mise.toml
└── .github/workflows/build.yml
The Python script uses only the standard library. It turns the Markdown files into JSON that the frontend imports:
# scripts/build_content.py
import json, os, pathlib
src = pathlib.Path(os.environ.get("CONTENT_DIR", "content"))
out = pathlib.Path("src/generated/content.json")
out.parent.mkdir(parents=True, exist_ok=True)
items = [{"slug": p.stem, "body": p.read_text(encoding="utf-8")}
for p in sorted(src.glob("*.md"))]
out.write_text(json.dumps(items, indent=2), encoding="utf-8")
print(f"wrote {len(items)} items to {out}")
This is the full mise.toml. Later sections refer back to it:
[tools]
node = "24"
python = "3.14"
[settings]
lockfile = true
[env]
CONTENT_DIR = "content"
VITE_SITE_NAME = "Docs Preview"
[tasks.install]
description = "Install JS dependencies"
run = "npm ci"
[tasks."build:content"]
description = "Convert content files to JSON for the frontend"
run = "python scripts/build_content.py"
[tasks.build]
description = "Generate content, then build the Vite app"
depends = ["install", "build:content"]
run = "npx vite build"
The [tools] table uses version prefixes (Node 24 LTS, Python 3.14). The lockfile, covered below, records the exact patch versions they resolve to.
What Happens When You cd In and Out
With mise activated, entering a directory that contains a mise.toml puts that project’s pinned runtimes on your PATH. Leaving it restores whatever versions were active before. Once mise activate is in your shell startup file, mise swaps tools and environment variables each time you change directory.
cd ~
node --version # whatever your system or nvm provides
cd ~/code/docs-preview
node --version # v24.x from mise.toml
python3 --version # Python 3.14.x
echo "$CONTENT_DIR" # content
When mise switches versions depends on the shell. Per the shims documentation, bash, zsh, fish and xonsh switch as soon as you run cd. Other shells wait until the next prompt appears.
When a Pinned Version Isn’t Installed Yet
On a fresh machine, run mise install in the project directory. That is the explicit, predictable fix. Tasks also handle it for you: mise tasks run with the project’s tools and env vars, and the auto_install setting, which is on by default, installs any missing configured tools when you call mise run. Python needs care if you rely on shims. The settings reference explains that a shim which can’t resolve a mise-managed version (for example, when auto-install is turned off) falls back to the first program with the same name elsewhere on PATH. For a tool the OS also ships, like python3, that means your pinned version never runs and you get no error.
To pin a different version in one terminal only, use mise shell node@<version>. This needs a shell where mise is already activated.
Existing .nvmrc and .python-version Files
mise does not read .nvmrc or .python-version unless you turn it on for each tool with idiomatic_version_file_enable_tools. The mise FAQ confirms these idiomatic version files are off by default. That default arrived in mise 2025.10.0. To opt in:
[settings]
idiomatic_version_file_enable_tools = ["node", "python"]
The configuration docs list package.json among the files mise reads for node, so enabling node can pick that file up too. To skip a single file, add a tool:filename pair such as node:package.json to the idiomatic_version_file_disable_files setting.
Once mise.toml pins both runtimes, keep .nvmrc only for teammates still on nvm, and make sure the two files agree. If you need a refresher on how nvm treats that file, see the guides for switching Node versions with nvm on macOS, Linux and Windows.
Teammates and CI
A laptop and a CI runner read the same mise.toml but get tools onto PATH in different ways:
| Laptop | GitHub Actions | |
|---|---|---|
| Tools on PATH | Activation hook in the shell | jdx/mise-action installs mise and pinned tools |
| Version source | mise.toml + mise.lock | Same files |
| Build command | mise run build | mise run build |
A teammate’s first clone:
- Install mise:
curl https://mise.run | sh - Add
eval "$(~/.local/bin/mise activate zsh)"(or the line for your shell) to your shell rc file. - Review
mise.toml, then runmise install. - Run
mise run build.
mise’s trust model needs care because config files can execute code. According to the getting-started guide, outside CI and paranoid mode, any command that acts on the project, such as mise install, mise exec or mise run, trusts the active config automatically. So review mise.toml before you run one, and use mise trust when you want to trust a config explicitly.
Lock exact versions. The lockfile, mise.lock, is created by mise lock or by the lockfile = true setting shown above. Commit it. With mise install --locked, a missing lock entry becomes an error, so mise never quietly picks a new version.
CI workflow:
# .github/workflows/build.yml
name: build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: jdx/mise-action@v4
- run: mise run build
The mise-action step installs mise and the tools in mise.toml, so the workflow uses the same versions developers use locally. By default it runs mise install (install: true) and caches tools (cache: true). When mise.lock is present, it installs with --locked. It also passes ${{ github.token }} automatically. On laptops, setting GITHUB_TOKEN or MISE_GITHUB_TOKEN helps if installs hit rate limits, as described on the GitHub tokens page.
When something breaks, three commands narrow it down:
| Symptom | Command |
|---|---|
| Activation or install looks wrong | mise doctor |
| Wrong config file seems to apply | mise config ls |
| Wrong version selected | mise ls --current |
A Real Task: mise run build Across Two Runtimes
A task defined in mise.toml runs with the project’s pinned tools and [env] variables already applied. That means mise run build uses the same Node and Python on a laptop and in CI, even without shell activation.
The depends key makes build wait until every task it lists has finished, so the Python step has written src/generated/content.json before Vite reads it. The listed tasks can run concurrently. The tasks docs state that prerequisites may run in parallel, and that their order in depends does not establish a sequence. That is safe here because npm ci writes node_modules and the Python script writes src/generated, so neither step touches the other’s output. If one step ever consumes the other’s output, make it depend on that step directly.
mise tasks # lists install, build:content, build
mise run build # npm ci + Python step, then vite build
The running tasks guide sets the default limit at eight parallel jobs.
Per-Project Env Vars With mise
Values in the [env] table are plain text in a committed file. mise env is configuration management, not secret management. In the example, the Python script reads CONTENT_DIR through os.environ, and the frontend reads import.meta.env.VITE_SITE_NAME.
The VITE_ prefix makes the risk of plaintext [env] values concrete. The Vite env docs explain that any variable starting with VITE_ ends up in the client bundle, with its value written into the code at build time. Any VITE_ value in mise.toml is public once the site ships.
A missing build-time variable does not fail the build. You get a bundle that calls the wrong URL. Session replay can show this from the user’s side: you watch requests go to an undefined base URL, then trace the problem back to the environment the build ran in.
When Is mise Worth It?
If a project only needs Node, nvm and an .nvmrc already do the job, and mise is optional. The nvm guides cover that setup well. mise is worth adopting when one repository pins two or more runtimes and CI has to match every laptop. To try it on a repo you already have, add a mise.toml with [tools] pins, run mise lock, commit both files, and change your CI build step to mise run build.
FAQs
Where should I keep secrets if mise.toml is committed?
Keep secrets in a gitignored mise.local.toml, or declare them in mise.toml with required = true, which makes mise check that the variable is set without storing a value. Adding redact = true masks values in captured task output, but mise env still prints them in plaintext. For secrets that must be committed, mise can decrypt SOPS files encrypted with age (an experimental feature) for mise exec, tasks and shell activation.
Does mise read asdf .tool-versions files?
Yes. Unlike .nvmrc, mise reads .tool-versions without any extra settings. On Unix it can also run asdf shell plugins, though not every asdf command or plugin is guaranteed to work. If mise.toml and .tool-versions in the same directory declare the same tool, mise.toml wins. The mise docs advise against sharing install directories between asdf and mise, so run mise install after you switch.
Why does my editor or script use a different Node version than my terminal?
Editors, git hooks and schedulers often don't load your interactive rc file, so the mise activation hook never runs. To fix this, add mise activate zsh --shims (or the line for your shell) to a login profile your editor reads, such as ~/.zprofile, and keep plain mise activate in ~/.zshrc. For scripts, use mise exec -- command or mise run instead. Shim mode skips some activation features, such as most hooks.