12k
All articles

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.

OpenReplay Team
OpenReplay Team
An Introduction to mise, the Multi-Language Version Manager

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.toml puts that project’s pinned runtimes on PATH, and leaving it restores the previous versions.
  • mise ignores .nvmrc and .python-version unless you turn them on per tool with the idiomatic_version_file_enable_tools setting.
  • mise run loads the project’s tools and env itself and installs missing tools by default, so it works in CI without shell activation.
  • jdx/mise-action@v4 installs mise and the tools in mise.toml. If a mise.lock is 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:

LaptopGitHub Actions
Tools on PATHActivation hook in the shelljdx/mise-action installs mise and pinned tools
Version sourcemise.toml + mise.lockSame files
Build commandmise run buildmise run build

A teammate’s first clone:

  1. Install mise: curl https://mise.run | sh
  2. Add eval "$(~/.local/bin/mise activate zsh)" (or the line for your shell) to your shell rc file.
  3. Review mise.toml, then run mise install.
  4. 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:

SymptomCommand
Activation or install looks wrongmise doctor
Wrong config file seems to applymise config ls
Wrong version selectedmise 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.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.