The Version Mismatch Problem That Wastes Your Afternoons
Your colleague pushed a Terraform plan that works perfectly on their machine. You pull it, run terraform apply, and hit a cryptic error. Three hours later, you discover they’re on Terraform 1.7 and you’re on 1.5. Sound familiar?
This is one of the most common, most annoying, and most avoidable pain points in DevOps. The root cause is simple: teams lack a shared, enforced contract for which version of each tool everyone — and every pipeline — should use. .nvmrc helps for Node.js. pyenv helps for Python. But what about kubectl? Terraform? Helm? You end up with a zoo of version managers, each with its own syntax, activation mechanism, and CI integration story.
mise (pronounced “meez”, short for mise en place) solves this with a single config file and a single tool. I’ve applied this approach in production across multiple teams and the results have been consistently stable — no more “works on my machine” Terraform surprises.
Quick Start: Up and Running in 5 Minutes
Install mise
On Linux and macOS, the fastest way is the official installer:
# Linux / macOS
curl https://mise.run | sh
# Or via Homebrew (macOS)
brew install mise
Then activate it in your shell. Add this to your ~/.bashrc, ~/.zshrc, or equivalent:
# For bash
echo 'eval "$(mise activate bash)"' >> ~/.bashrc
source ~/.bashrc
# For zsh
echo 'eval "$(mise activate zsh)"' >> ~/.zshrc
source ~/.zshrc
Define your tools in a config file
Navigate to your project root and create a .mise.toml file:
[tools]
terraform = "1.8.5"
kubectl = "1.30.2"
node = "20.14.0"
python = "3.12.3"
helm = "3.15.2"
Then install everything declared in that file with one command:
mise install
mise downloads, caches, and activates all listed versions. From that point on, any terminal session inside the project directory automatically uses these exact versions — no manual switching, no export statements, no activation scripts to remember.
Verify it worked:
mise current
# terraform 1.8.5 .mise.toml
# kubectl 1.30.2 .mise.toml
# node 20.14.0 .mise.toml
# python 3.12.3 .mise.toml
Deep Dive: How mise Actually Works
The config file cascade
mise reads .mise.toml files starting from the current directory and walking up toward $HOME. This means you can have global defaults at ~/.config/mise/config.toml and project-specific overrides at the repo level. Project config always wins.
It also understands legacy version files — if your project already has an .nvmrc, .node-version, .python-version, or .terraform-version, mise picks those up automatically without any migration.
Shims vs PATH injection
mise uses PATH injection (not shims like asdf). When you run eval "$(mise activate bash)", it prepends a managed directory to your PATH. This is faster and more transparent — which terraform gives you a real binary path, not a shim wrapper. No mysterious slowdowns on every command invocation.
Plugin ecosystem
mise uses the same plugin registry as asdf, which means it supports 400+ tools out of the box. Installing a tool not in the core list is as simple as:
# Install a specific tool version interactively
mise use [email protected]
# List all available versions for a tool
mise ls-remote terraform
# See what's installed globally
mise ls
Advanced Usage: CI/CD Integration
GitHub Actions
This is where mise really pays off. Instead of pinning tool versions in three different places (local, CI yaml, Dockerfile), everything comes from the single .mise.toml in the repo.
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install mise
uses: jdx/mise-action@v2
# Automatically reads .mise.toml and installs all tools
- name: Verify versions
run: mise current
- name: Terraform plan
run: |
terraform init
terraform plan
The jdx/mise-action action handles caching too — it stores downloaded tool binaries between runs, so your pipeline doesn’t re-download Terraform from scratch every time.
GitLab CI
default:
before_script:
- curl https://mise.run | sh
- echo 'export PATH="$HOME/.local/share/mise/shims:$PATH"' >> ~/.bashrc
- source ~/.bashrc
- mise install
deploy:
script:
- terraform init
- terraform apply -auto-approve
For repeated use, build a base Docker image with mise pre-installed rather than running the install script on every job.
Dockerfile integration
FROM ubuntu:24.04
# Install mise
RUN curl https://mise.run | sh
ENV PATH="/root/.local/share/mise/shims:/root/.local/bin:$PATH"
WORKDIR /app
# Copy version config first for better layer caching
COPY .mise.toml .
RUN mise install
COPY . .
RUN terraform init
Environment variables for CI
mise respects environment-based configuration, which is handy for overriding versions in specific pipeline stages without editing the config file:
# Override a specific tool version via env var
MISE_TERRAFORM_VERSION=1.7.0 terraform plan
# Or set globally for the job
export MISE_TERRAFORM_VERSION=1.7.0
Practical Tips from Production Use
Commit .mise.toml, never .tool-versions
mise supports both .mise.toml and the asdf-compatible .tool-versions format. Always prefer .mise.toml for new projects — TOML gives you richer configuration options (environment variables, task definitions, hooks) that the flat .tool-versions format can’t express.
Use mise tasks for project scripts
One underused feature: mise can replace your Makefile or ad-hoc shell scripts with typed, documented tasks:
[tools]
terraform = "1.8.5"
kubectl = "1.30.2"
[tasks.plan]
description = "Run terraform plan for staging"
run = "terraform -chdir=infra/staging plan"
[tasks.deploy]
description = "Apply and update kubeconfig"
run = [
"terraform -chdir=infra/staging apply -auto-approve",
"aws eks update-kubeconfig --name my-cluster"
]
# List available tasks
mise tasks
# Run a task
mise run plan
mise run deploy
Now your entire team runs the same commands using the same tool versions. New team members clone the repo, run mise install, and they’re productive in minutes.
Set project-scoped environment variables
mise also manages environment variables per project — a clean replacement for direnv in many cases:
[tools]
terraform = "1.8.5"
[env]
TF_WORKSPACE = "staging"
AWS_PROFILE = "my-company-staging"
KUBECONFIG = "./kubeconfig"
These variables activate automatically when you enter the directory and deactivate when you leave. No more forgetting to export AWS_PROFILE and accidentally deploying to production from the wrong context.
Trust configuration for shared machines
On shared servers or CI agents, mise requires you to explicitly trust config files before it loads them (a security feature to prevent malicious .mise.toml files from running arbitrary scripts):
# Trust a specific config file
mise trust /path/to/project/.mise.toml
# Or trust all configs in CI (use only in controlled environments)
export MISE_TRUSTED_CONFIG_PATHS="/path/to/project/.mise.toml"
Check version drift across the team
Add a CI step that explicitly verifies versions match the declared config — useful as a gate before expensive plan/apply steps:
#!/bin/bash
# scripts/check-versions.sh
set -e
EXPECTED_TF=$(mise current terraform 2>/dev/null | awk '{print $2}')
ACTUAL_TF=$(terraform version -json | python3 -c "import sys,json; print(json.load(sys.stdin)['terraform_version'])")
if [ "$EXPECTED_TF" != "$ACTUAL_TF" ]; then
echo "Version mismatch: expected $EXPECTED_TF, got $ACTUAL_TF"
exit 1
fi
echo "All versions match mise config"
Migration from asdf, nvm, pyenv, tfenv
If you’re already using these tools, migration is low-risk:
- asdf: mise reads
.tool-versionsautomatically. Just install mise and it picks up your existing config. Remove asdf when you’re confident. - nvm: mise reads
.nvmrcand.node-version. Runmise installand both tools coexist during transition. - pyenv: Reads
.python-version. Same story — coexistence is fine during migration. - tfenv / tgenv: No automatic detection, but migration is just adding
terraform = "x.y.z"to.mise.toml.
The migration strategy I’ve used in production: add .mise.toml to the repo alongside existing version files, then remove the old files in a follow-up PR once the team has confirmed everything works.
When mise Might Not Be the Right Fit
It’s honest to acknowledge the edges. mise manages tool versions, not runtime dependencies like npm packages or pip packages. You still need npm install and pip install -r requirements.txt for that. And if your team is fully standardized on a container-first workflow where every command runs inside Docker, the benefit is smaller (though mise still helps on the host for the tools that manage the containers).
For language runtimes with complex virtual environment semantics (especially Python), mise activates the Python version but you still manage virtualenvs separately — though mise use [email protected] combined with the [env] block for VIRTUAL_ENV can cover most cases cleanly.

