Eliminating team runtime version drift with a single `.mise.toml` — from Node.js, Bun, and Python to CI and Docker
If you've ever heard "it builds on my machine" in a team project, it's almost always a runtime version mismatch. One teammate uses Node 18 via nvm, another uses 20 via asdf, and the CI server quietly runs 22. There's a .nvmrc, but teammates on asdf walk right past it, and a Node version pinned with volta can mysteriously conflict with a pyenv virtual environment — this actually happens.
mise (pronounced "meez", French for "placed") is a polyglot runtime version manager written in Rust. It consolidates the roles of nvm, asdf, pyenv, and volta — previously scattered across individual tools — into a single .mise.toml file, and that file is read identically by local dev, GitHub Actions, and Docker builds, eliminating environment drift. This document is written against the latest version (which has moved to CalVer); check the official repository directly for current version information.
Before You Start: Installing mise and Registering Shell Hooks
To follow the rest of this guide, you first need to install mise and register the shell hooks. Without the hooks, running mise install won't switch versions.
macOS (Homebrew)
brew install miseLinux / macOS (official install script)
curl https://mise.run | shRegistering shell hooks
After installation, add the following line to your shell config file and restart your terminal.
# ~/.zshrc
eval "$(mise activate zsh)"# ~/.bashrc
eval "$(mise activate bash)"Once the hook is registered, mise reads .mise.toml whenever you change directories and automatically activates the appropriate version.
Core Concepts
The Three Roles of .mise.toml
mise divides a single configuration file into three axes.
| Feature | Role | Legacy replacement |
|---|---|---|
| Runtime version management | Automatic per-directory tool version switching | nvm, asdf, pyenv, rbenv, volta |
| Environment variable management | Per-project environment variable declarations | direnv |
| Task runner | Build, test, and deployment script definitions | make, npm scripts |
A basic .mise.toml looks like this:
[tools]
node = "22"
bun = "1.2"
python = "3.13"
[env]
NODE_ENV = "development"
PORT = "3000"mise also recognizes existing .nvmrc, .python-version, and .tool-versions files, so your existing files continue to work during migration. It is also compatible with most asdf plugins.
Version Range Specification Strategy
Behavior differs based on the version string format. Choose one of the three options.
# Fully pinned — when reproducibility of production builds matters most
node = "22.4.0"# Auto-receive patch versions — gets the latest patch within the 22.4.x range
node = "22.4"# Auto-receive minor and patch — automatically applies security patches
node = "22"A common real-world practice is to pin only the major version in team development environments to receive security patches automatically, while specifying the full version in production deployment images.
Configuration File Priority
mise merges multiple layers of configuration files in order. Priority increases going down, and later-declared values override earlier ones.
This hierarchy lets you separate team-shared settings from personal settings at the file level.
Automatic Version Switching on Directory Change
With the shell hook registered, changing directories triggers version switching via the following flow:
Because it's implemented in Rust with no shell eval-based initialization overhead, version switching is faster than nvm or asdf. The moment you cd from project A (Node 18) into project B (Node 22), the version switches instantly.
The Trust Model: Why You See a Trust Prompt
Understanding this section first makes everything else in the guide connect meaningfully.
The [env], [hooks], and [tasks] sections of .mise.toml can execute arbitrary code. For this reason, mise asks you to confirm trust the first time it reads a file containing these sections. There are two ways to handle this:
mise trust: Permanently registers the file in the trust list. Once run, no further prompts appear. Suitable for local development environments and Dev Containers.MISE_YES=1: Automatically accepts all prompts for a single command execution. Does not permanently save to the trust list, making it suitable for stateless environments like Docker builds or CI.
The sections below clearly distinguish which approach to use in each environment.
Practical Application
Setting Up the Team Development Environment: Separating Shared and Personal Configuration
The key to team configuration is separating what gets committed from what gets git-ignored at the file level.
# .mise.toml — committed to git
[tools]
node = "22.4"
bun = "1.2"
python = "3.13"
[env]
NODE_ENV = "development"
_.file = ".env.local" # Load .env.local contents as environment variables# mise.local.toml — git-ignored (for personal tool config overrides)
[tools]
node = "20" # When a specific teammate needs to test a different version# .env.local — git-ignored (plain environment variable file)
DATABASE_URL=postgres://localhost:5432/mydb_dev
API_SECRET=local-secretmise.local.toml is for personally overriding mise configuration itself (tool versions, tasks, etc.), while .env.local holds plain environment variables like secrets. A single _.file = ".env.local" line loads .env.local, so there's no need to install direnv separately.
It's a good idea to add these to .gitignore upfront.
echo "mise.local.toml" >> .gitignore
echo ".env.local" >> .gitignoreNew teammate onboarding now looks like this:
git clone https://github.com/your-org/your-repo
cd your-repo
mise trust # Register trust if using [env]/[tasks]
mise install # Install all tools declared in .mise.toml at onceThis can replace a significant portion of multi-page "dev environment setup" documentation.
GitHub Actions Integration
Using the official jdx/mise-action reads .mise.toml directly in CI. In the example below, mise run build and mise run test execute tasks defined in the [tasks] section of .mise.toml (see the "Task Runner" section below).
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v4
with:
cache: true
github_token: ${{ secrets.GITHUB_TOKEN }}
- run: mise run build
- run: mise run testSetting cache: true caches already-downloaded tools and reduces run time. jdx/mise-action handles trust internally, so no separate mise trust or MISE_YES=1 is needed. Check the jdx/mise-action repository for the actual latest version.
Docker Integration
In stateless environments like Docker builds, use MISE_YES=1. Tools installed by mise must have their shim path added to PATH to be found in CMD or RUN instructions.
FROM debian:12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates && \
rm -rf /var/lib/apt/lists/* && \
curl https://mise.run | sh
# Add the mise binary and shim paths to PATH
ENV PATH="/root/.local/bin:/root/.local/share/mise/shims:$PATH"
WORKDIR /app
COPY .mise.toml .
RUN MISE_YES=1 mise install
COPY . .
CMD ["node", "server.js"]The key is fetching the binary with the official install script (curl https://mise.run | sh) and registering the shim path with ENV PATH. Without the shim path, you'll get node not found errors even after a successful mise install.
Dev Containers Integration
Registering in the postCreateCommand of .devcontainer/devcontainer.json installs everything automatically when the container is created. Since trust can be persisted just like on a local machine, run mise trust first.
{
"postCreateCommand": "curl https://mise.run | sh && mise trust && mise install"
}Task Runner
Defining team-wide scripts in the [tasks] section lets you run them with mise run <task>. The mise run build and mise run test calls in the GitHub Actions example reference the definitions below.
[tasks.build]
run = "bun run build"
[tasks.test]
run = "bun test"
depends = ["build"]
[tasks.dev]
run = "bun run dev"
[tasks.lint]
run = "bun run lint"Declaring depends guarantees task execution order. Cross-package task references in monorepo environments are also supported, enabling complex monorepo build orchestration. See the mise tasks documentation for detailed syntax.
Pros and Cons
Advantages
| Item | Description |
|---|---|
| Single file | Manage runtime, environment variables, and tasks with one .mise.toml, eliminating config file fragmentation |
| Fast switching | No shell eval-based initialization, so version switching is faster than nvm or asdf |
| Backward compatibility | Recognizes most asdf plugins, .nvmrc, .python-version, and .tool-versions |
| Team consistency | Using the same config file for CI, local, and Docker eliminates version mismatch issues |
| Environment variable integration | Per-project environment variable management without direnv |
| Task runner | Can replace make and npm run, with cross-platform task definitions |
| Renovate support | Automatically creates PRs to update tool versions in .mise.toml |
Disadvantages and Caveats
| Item | Description |
|---|---|
| Trust model must be understood | [env], [hooks], and [tasks] can execute arbitrary code; appropriate trust handling per environment is required |
| Supply chain security | Downloads binaries from multiple backends (GitHub Releases, npm, PyPI, etc.) and does not guarantee hash verification by default; consider using a lockfile (.mise.lock) if fully reproducible builds are required |
| Windows limitations | Some asdf plugins are UNIX-only and may not work on Windows |
| Learning curve | Developers coming from nvm alone may find the integrated task and environment variable concepts a barrier to entry |
Common Mistakes in Practice
1. Shell hooks not registered
If version switching doesn't work after running mise install, it's almost always a shell hook issue. Verify that you didn't skip the hook registration step in the Before You Start section after installation.
2. Not adding mise.local.toml and .env.local to .gitignore
To prevent files containing personal secrets from being accidentally committed, add them as soon as you set up the project.
echo "mise.local.toml" >> .gitignore
echo ".env.local" >> .gitignore3. Not adding the shim path to PATH in Docker or custom CI
Even if mise install succeeds, node or python commands won't be found if the shim path isn't in PATH. GitHub Actions using jdx/mise-action handles this automatically, but you must add it explicitly when writing your own Dockerfile or configuring another CI environment.
Closing Thoughts
The moment you commit .mise.toml to version control, that file becomes your team's runtime contract. Local development, GitHub Actions, Docker builds, and Dev Containers all read the same file.
If you're starting right now, here's the recommended order of approach:
- Install and register shell hooks — Install with
brew install mise(macOS) orcurl https://mise.run | sh, then addeval "$(mise activate zsh)"to your shell config file - Create and commit
.mise.toml— Declare your project's current runtime versions in.mise.toml, verify withmise trust && mise install, then commit to the team repo - CI integration — Add
jdx/mise-actionto your GitHub Actions workflow and the version gap between local and CI disappears
References
- mise-en-place official documentation
- GitHub: jdx/mise
- GitHub: jdx/mise-action
- Continuous Integration | mise-en-place
- mise Docker Cookbook
- Getting Started with Mise | Better Stack
- Mise vs asdf | Better Stack
- Can Mise replace Volta?
- Automating Development Environment with Mise
- Best Practices for Using Mise
- Automated Dependency Updates for mise | Renovate Docs
- How to Use mise for Tool Version Management
- mise and Dev Containers Setup Guide