Most developers carry a silent tax: the cognitive overhead of juggling version managers. nvm for Node, pyenv for Python, rbenv or asdf for Ruby, sdkman for Java/Kotlin, tfenv for Terraform, goenv for Go. Each has its own install process, its own shell integration, its own config file format, and its own quirks. Switching between projects means remembering which version manager controls which runtime, why your shell feels slow, and why node --version returns something unexpected.
Mise (pronounced “meez,” from mise en place) solves this completely. It’s a single Rust binary that manages every runtime through a unified interface, loads .mise.toml config files automatically per directory, handles environment variables alongside tool versions, and includes a task runner. It’s backwards compatible with .tool-versions files (the asdf format), making migration trivial.
This guide covers installation, core workflows, team configuration, and the patterns that make mise genuinely transformative for developer experience.
The Problem with the Current Ecosystem
Here’s a typical senior developer’s shell profile:
1
2
3
4
5
6
7
8
9
10
11
12
|
# .zshrc or .bashrc — the usual mess
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # ~200ms
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
eval "$(pyenv init --path)" # ~50ms
eval "$(pyenv init -)" # ~50ms
eval "$(rbenv init -)" # ~50ms
export SDKMAN_DIR="$HOME/.sdkman"
[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && \
source "$HOME/.sdkman/bin/sdkman-init.sh" # ~200ms
|
This adds 500ms+ to every new shell. Five different tools mean five sets of shims, five caches to manage, and five breaking updates to handle. When a new developer joins, they spend their first day installing and configuring all of them — or worse, they skip the pinning entirely and run whatever global version happens to be installed.
Mise replaces all of them with a single binary and sub-30ms shell startup.
Installing Mise
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# The quickest path — works on macOS and Linux
curl https://mise.run | sh
# macOS via Homebrew
brew install mise
# Cargo (if you have Rust)
cargo install mise
# Arch Linux
pacman -S mise
# Windows (PowerShell)
winget install mise
|
Add the shell hook — this is what enables automatic version switching when you change directories:
1
2
3
4
5
6
7
8
9
10
11
|
# Bash — add to ~/.bashrc
echo 'eval "$(mise activate bash)"' >> ~/.bashrc
# Zsh — add to ~/.zshrc
echo 'eval "$(mise activate zsh)"' >> ~/.zshrc
# Fish — add to ~/.config/fish/config.fish
echo 'mise activate fish | source' >> ~/.config/fish/config.fish
# Then reload
source ~/.zshrc # or restart your terminal
|
Verify:
1
2
|
mise --version
# mise 2024.x.x
|
Core Concepts
Mise has three main features that work together:
- Tools: versioned runtimes and CLIs (Node, Python, Go, Terraform, kubectl, etc.)
- Environments: per-directory environment variables that activate automatically
- Tasks: a built-in task runner (similar to
make or task)
Everything is configured in .mise.toml at the project root (or globally at ~/.config/mise/config.toml).
Managing Runtimes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Install a specific version
mise install node@22.0.0
mise install python@3.12.3
mise install go@1.22.1
# Install the latest stable
mise install node@latest
mise install python@latest
# Use a short version (mise resolves to latest patch)
mise install node@22
mise install python@3.12
# Install multiple tools at once
mise install node@22 python@3.12 go@1.22
|
1
2
3
4
5
6
7
8
9
10
11
|
# Set versions for the current directory (writes to .mise.toml)
mise use node@22
mise use python@3.12
mise use go@1.22
# Set a global fallback version (used when no local config exists)
mise use --global node@22
mise use --global python@3.12
# Install + use in one command
mise use --pin node@22.0.0 # Pins the exact version in .mise.toml
|
The .mise.toml File
Running mise use creates or updates .mise.toml in the current directory. Commit this file — it’s the source of truth for what versions the project requires:
1
2
3
4
5
6
7
8
|
# .mise.toml
[tools]
node = "22.0.0"
python = "3.12.3"
go = "1.22.1"
terraform = "1.8.0"
kubectl = "1.29.0"
helm = "3.14.0"
|
When any team member enters this directory, mise automatically activates these versions. No nvm use, no pyenv local, no manual steps.
Checking Status
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# See what's active in the current directory
mise current
# node 22.0.0 ~/.local/share/mise/installs/node/22.0.0/bin/node
# python 3.12.3 ~/.local/share/mise/installs/python/3.12.3/bin/python3
# go 1.22.1 ~/.local/share/mise/installs/go/1.22.1/bin/go
# Check if any tools are missing (needs install)
mise doctor
# ✓ mise is up to date
# ✓ shims are up to date
# ✗ node@22.0.0 is not installed, run `mise install`
# Install everything declared in .mise.toml
mise install
|
Version Ranges and Fuzzy Matching
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
[tools]
# Exact version
node = "22.0.0"
# Latest 22.x patch release
node = "22"
# Latest LTS
node = "lts"
# Latest of any version
node = "latest"
# Prefix match
node = "22.0"
|
For CI environments where reproducibility matters, always pin exact versions. For local development, fuzzy versions are fine.
The Plugin System
Mise uses a plugin system (built on the asdf plugin ecosystem) to support hundreds of tools. Most popular tools work out of the box:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# See all available tools
mise registry
# Some highlights:
mise install node # Node.js via node plugin
mise install python # CPython via python plugin
mise install go # Go compiler
mise install ruby # Ruby via ruby-build
mise install java # JDK (multiple vendors: temurin, graalvm, corretto)
mise install terraform # HashiCorp Terraform
mise install kubectl # Kubernetes CLI
mise install helm # Helm package manager
mise install awscli # AWS CLI
mise install gcloud # Google Cloud SDK
mise install rust # Rust via rustup
mise install deno # Deno runtime
mise install bun # Bun runtime
mise install zig # Zig compiler
mise install dotnet # .NET SDK
|
Java: Multiple Vendors
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# List available Java distributions
mise list-remote java | head -20
# temurin-21.0.3+9
# corretto-21.0.2.13.1
# graalvm-21.0.2+13.1
# zulu-21.34.19
# Install specific vendor
mise install java@temurin-21.0.3+9
mise install java@corretto-21
# Use GraalVM for native image compilation
mise install java@graalvm-21
|
Custom Plugins
If a tool isn’t in the registry, add it as a custom plugin or use the ubi (Universal Binary Installer) backend:
1
2
3
4
5
6
7
8
9
10
|
# .mise.toml — install any GitHub release binary
[tools]
# Install from GitHub releases via ubi backend
"ubi:cli/cli" = "latest" # GitHub CLI
"ubi:derailed/k9s" = "0.32.4" # k9s Kubernetes TUI
"ubi:jesseduffield/lazygit" = "latest"
# Or point to a custom plugin repository
[plugins]
my-internal-tool = "https://github.com/myorg/mise-plugin-my-tool"
|
Environment Variables
This is where mise goes beyond other version managers. Alongside tool versions, .mise.toml can define environment variables that activate when you enter the directory — project-local config without .env files scattered everywhere.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# .mise.toml
[tools]
node = "22"
python = "3.12"
[env]
# Project-specific env vars — loaded when you cd into this directory
NODE_ENV = "development"
DATABASE_URL = "postgres://localhost:5432/myapp_dev"
REDIS_URL = "redis://localhost:6379/0"
API_BASE_URL = "http://localhost:8080"
LOG_LEVEL = "debug"
# Reference other env vars
APP_NAME = "myapp"
APP_LOG_DIR = "{{env.HOME}}/logs/{{env.APP_NAME}}"
|
When you cd out of the directory, these variables are unset. When you return, they’re set again. No more “why is my DATABASE_URL pointing at production?” mysteries.
Secrets and .env Integration
For sensitive values, keep secrets out of .mise.toml (which gets committed) and load them from a .env file:
1
2
3
4
5
6
7
|
# .mise.toml — safe to commit
[env]
_.file = ".env" # Load a .env file
_.file = [".env", ".env.local"] # Load multiple files in order
NODE_ENV = "development"
API_BASE_URL = "http://localhost:8080"
|
1
2
3
|
# .env — in .gitignore
DATABASE_URL=postgres://user:secretpassword@localhost:5432/myapp
STRIPE_SECRET_KEY=sk_test_xxxxx
|
Dynamic Environment Variables
Use shell commands to compute env var values at activation time:
1
2
3
4
5
6
7
8
9
|
[env]
# Set the AWS profile based on a file in the project
AWS_PROFILE = "{{exec('cat .aws-profile 2>/dev/null || echo default')}}"
# Use git to set a version variable
APP_VERSION = "{{exec('git describe --tags --always --dirty 2>/dev/null || echo dev')}}"
# Point to the mise-managed tool's directory
JAVA_HOME = "{{exec('mise where java')}}"
|
PATH Manipulation
1
2
3
4
5
6
7
|
[env]
# Prepend project-local binaries to PATH
_.path = ["./bin", "./scripts", "./node_modules/.bin"]
# Or set GOPATH relative to the project
GOPATH = "{{cwd}}/.gopath"
_.path = ["{{cwd}}/.gopath/bin"]
|
Tasks: The Built-In Task Runner
Mise includes a task runner that replaces make, npm run, or task (Taskfile) for simple workflows. Tasks are defined in .mise.toml and can depend on each other.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
# .mise.toml
[tools]
node = "22"
python = "3.12"
[tasks.install]
description = "Install all dependencies"
run = """
npm ci
pip install -r requirements.txt
"""
[tasks.dev]
description = "Start the development server"
depends = ["install"]
run = "npm run dev"
[tasks.test]
description = "Run all tests"
run = """
npm test
pytest tests/ -v
"""
[tasks.lint]
description = "Lint all code"
run = """
npm run lint
ruff check .
mypy .
"""
[tasks.build]
description = "Build production artifacts"
depends = ["test", "lint"]
run = "npm run build"
[tasks.clean]
description = "Remove build artifacts"
run = "rm -rf dist/ __pycache__ .pytest_cache node_modules/.cache"
|
Run tasks with mise run (or mise r):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
mise run dev
mise run test
mise run build
# Short form
mise r dev
# List all tasks
mise tasks
# NAME DESCRIPTION
# build Build production artifacts
# clean Remove build artifacts
# dev Start the development server
# install Install all dependencies
# lint Lint all code
# test Run all tests
|
File-Based Tasks
For complex tasks, put them in a mise-tasks/ directory as shell scripts:
project/
├── .mise.toml
└── mise-tasks/
├── db-migrate # executable shell script
├── db-seed
└── deploy
1
2
3
4
5
6
7
8
|
#!/usr/bin/env bash
# mise-tasks/db-migrate
#MISE description="Run database migrations"
#MISE depends=["infra-up"]
set -euo pipefail
echo "Running migrations..."
migrate -path ./migrations -database "$DATABASE_URL" up
|
Team and Monorepo Configuration
Cascading Configuration
Mise loads configuration files up the directory tree, merging them. This makes monorepos natural:
my-monorepo/
├── .mise.toml # Global tools: go, terraform, kubectl
├── services/
│ ├── api/
│ │ └── .mise.toml # api-specific: node, extra env vars
│ └── worker/
│ └── .mise.toml # worker-specific: python
└── infra/
└── .mise.toml # infra-specific: terraform version override
1
2
3
4
5
6
7
8
9
|
# my-monorepo/.mise.toml — root level
[tools]
go = "1.22"
terraform = "1.8"
kubectl = "1.29"
[env]
CLUSTER_NAME = "my-cluster"
AWS_REGION = "us-east-1"
|
1
2
3
4
5
6
7
|
# my-monorepo/services/api/.mise.toml — inherits root, adds/overrides
[tools]
node = "22"
[env]
PORT = "8080"
SERVICE_NAME = "api"
|
When you’re in services/api/, you have Go, Terraform, kubectl, Node, and both sets of env vars. When you cd .. to the root, Node and the API env vars disappear.
New Developer Onboarding
With mise, onboarding becomes a single command:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# New developer clones the repo
git clone git@github.com:myorg/myproject.git
cd myproject
# Install mise (if not already installed)
curl https://mise.run | sh
echo 'eval "$(mise activate zsh)"' >> ~/.zshrc && source ~/.zshrc
# Install all tools declared in .mise.toml
mise install
# Everything is ready — correct Node, Python, Go, kubectl versions all active
node --version # 22.0.0
python --version # 3.12.3
go version # go1.22.1
|
Compare this to the old flow: “Install nvm, then run nvm install, then install pyenv, then pyenv install, then install rbenv…” — an hour of work that was documented on a wiki page nobody kept up to date.
CI/CD Integration
GitHub Actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install mise
uses: jdx/mise-action@v2
# Automatically installs mise and all tools from .mise.toml
# Also caches tool installations between runs
- name: Verify tool versions
run: |
node --version
python --version
go version
- name: Run tests
run: mise run test
|
The jdx/mise-action handles installing mise, reading .mise.toml, installing all tools, and caching them via GitHub Actions cache. Build times stay fast after the first run.
GitLab CI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# .gitlab-ci.yml
default:
before_script:
- curl https://mise.run | sh
- eval "$(~/.local/bin/mise activate bash)"
- mise install
test:
script:
- mise run test
build:
script:
- mise run build
artifacts:
paths: [dist/]
|
Docker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# Dockerfile — use mise to install exact tool versions in containers too
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y curl git && rm -rf /var/lib/apt/lists/*
# Install mise
RUN curl https://mise.run | sh
ENV PATH="/root/.local/bin:${PATH}"
WORKDIR /app
# Copy config and install tools — this layer is cached
COPY .mise.toml .
RUN mise install --no-shims
# The tools are now available via mise's shim directory
ENV PATH="/root/.local/share/mise/shims:${PATH}"
COPY . .
RUN mise run build
CMD ["mise", "run", "start"]
|
Set fallback versions used when no local .mise.toml is present:
1
2
3
4
5
6
7
8
|
# Set global defaults
mise use --global node@22
mise use --global python@3.12
mise use --global terraform@1.8
mise use --global kubectl@1.29
# This writes to ~/.config/mise/config.toml
cat ~/.config/mise/config.toml
|
1
2
3
4
5
6
7
8
9
10
11
12
|
# ~/.config/mise/config.toml
[tools]
node = "22"
python = "3.12"
terraform = "1.8"
kubectl = "1.29"
[settings]
# Always install tools on `mise use` without prompting
always_keep_download = true
# Show a hint when entering a directory with a .mise.toml
not_found_auto_install = true
|
Global Environment Variables
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# ~/.config/mise/config.toml
[env]
# Personal defaults that apply everywhere
EDITOR = "nvim"
VISUAL = "nvim"
PAGER = "less -R"
# Always use your personal AWS profile unless overridden
AWS_PROFILE = "personal"
# Global Go settings
GOPATH = "{{env.HOME}}/go"
_.path = ["{{env.HOME}}/go/bin"]
|
Migrating from asdf
Mise is a drop-in replacement for asdf and reads .tool-versions files natively:
1
2
3
4
5
6
7
8
9
10
11
12
|
# If you have .tool-versions, mise reads it automatically
cat .tool-versions
# nodejs 22.0.0
# python 3.12.3
# Check that mise resolves it correctly
mise current
# nodejs 22.0.0
# python 3.12.3
# Optionally migrate to .mise.toml format
mise migrate # Converts .tool-versions to .mise.toml
|
Most asdf plugins work with mise. The plugin URLs are compatible:
1
2
|
# Add an asdf plugin to mise
mise plugin install ruby https://github.com/asdf-vm/asdf-ruby
|
Migrating from nvm
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# Check your current nvm versions
nvm list
# In each project directory, set the mise version
mise use node@$(cat .nvmrc 2>/dev/null || nvm current)
# Remove nvm from shell profile after migration
# Delete from ~/.zshrc or ~/.bashrc:
# export NVM_DIR=...
# [ -s "$NVM_DIR/nvm.sh" ] && ...
# Uninstall nvm (optional — keep it while transitioning)
rm -rf "$NVM_DIR"
|
Mise uses shims — tiny wrapper scripts in ~/.local/share/mise/shims/ that intercept calls to node, python, etc. When you call node, the shim runs, finds the correct version from the nearest .mise.toml, and executes the real binary.
This is about 1–3ms overhead per command. Compare this to nvm’s approach of modifying PATH on every directory change (100–300ms) or pyenv’s approach of spawning a shell process per shim call (20–50ms).
Shell startup overhead is near zero because the hook is a single eval that registers a fast chpwd hook — there’s no slow plugin loading at shell startup like with nvm.
1
2
3
4
5
6
7
8
|
# Benchmark shell startup before and after
time (zsh -i -c exit)
# Before mise (with nvm + pyenv + rbenv):
# zsh -i -c exit 0.62s user 0.18s system 99% cpu 0.802 total
# After mise:
# zsh -i -c exit 0.08s user 0.04s system 99% cpu 0.119 total
|
Practical Patterns
Per-Project Python Virtual Environments
Mise can automatically create and activate a Python virtualenv:
1
2
3
4
5
6
7
|
# .mise.toml
[tools]
python = "3.12"
[env]
# Tell mise to create and activate a venv in .venv/
_.python.venv = { path = ".venv", create = true }
|
When you enter the directory, mise creates .venv/ if it doesn’t exist, installs the python version, and activates the venv. pip install goes into the project venv automatically.
Multiple Node Versions in a Monorepo
1
2
3
4
5
6
7
8
9
10
11
|
# root .mise.toml
[tools]
node = "22" # Default for most services
# frontend/.mise.toml — legacy frontend requires Node 18
[tools]
node = "18.20.2"
# new-service/.mise.toml — experimental, uses Node 23
[tools]
node = "23"
|
Each subdirectory activates its own Node version automatically.
Trusting Project Configs
When you enter a directory with a .mise.toml for the first time, mise asks you to trust it (a security measure against malicious repos):
1
2
|
mise trust # Trust the .mise.toml in the current directory
mise trust --all # Trust all configs (for CI environments)
|
In CI, set MISE_YES=1 to skip all prompts:
Comparison with Alternatives
| Feature |
mise |
asdf |
nvm+pyenv+rbenv |
devenv/nix |
| Languages supported |
500+ |
500+ |
3 tools, 3 ecosystems |
Everything |
| Shell startup overhead |
~5ms |
~50ms |
~500ms |
~100ms |
| Task runner |
✅ Built-in |
❌ |
❌ |
✅ (devshell) |
| Env var management |
✅ Built-in |
❌ |
❌ |
✅ |
| .env file loading |
✅ |
❌ |
❌ |
Partial |
| Windows support |
✅ |
❌ |
Partial |
Partial |
| .tool-versions compat |
✅ |
Native |
❌ |
❌ |
| Learning curve |
Low |
Low |
Medium |
High |
| Reproducibility |
Good |
Good |
Poor |
Excellent |
Use mise when: you want a fast, practical solution that works across all runtimes, requires no new mental model, and integrates into CI in minutes.
Use Nix/devenv when: you need perfect reproducibility including system libraries (openssl, libpq), you’re already invested in the Nix ecosystem, or you’re working on infrastructure where bit-for-bit reproducibility matters.
Conclusion
Mise is the kind of tool that you install once and then forget about — in the best way. Your shell is fast, the right runtime is always active, env vars are scoped to the projects that need them, and new team members are productive in minutes rather than an afternoon.
The migration from whatever mix of version managers you’re currently running takes an hour and immediately pays dividends in shell startup time alone. The .mise.toml file in each repository becomes a machine-readable contract: this project needs Node 22, Python 3.12, and these environment variables. No wiki page needed, no onboarding script to maintain.
Start with curl https://mise.run | sh, add one .mise.toml to your most-used project, and feel the difference in the first cd.
Comments