If you have ever juggled nvm, pyenv, rbenv, a pile of .env files, and a Makefile in the same project, mise (pronounced “meez”, short for mise-en-place) is worth a look. It replaces all of them with a single tool and a single mise.toml config file.
In this post I’ll introduce the three major concepts that make mise useful day to day:
- Dev tools — install and switch between runtimes like Node, Python, and Go.
- Environments — load the right environment variables per directory.
- Tasks — a built-in task runner for builds, tests, linting, and scripts.
Then I’ll walk through a small but fully runnable example that ties all three together.
What is mise?
mise is a polyglot tool manager, environment manager, and task runner rolled into one binary. It is a spiritual successor to asdf (and is compatible with asdf’s .tool-versions files), but written in Rust and considerably faster.
The core idea: you declare what a project needs in a mise.toml file, and mise makes sure those tools, versions, and environment variables are active whenever you’re inside that directory.
Install mise
On Linux or macOS:
| |
By default mise installs to ~/.local/bin. Verify it:
| |
See the installation guide for other methods: Homebrew,
apt,dnf, Snap, Nix, Windows, and more.
Activate mise (recommended)
mise exec works for one-off commands, but for interactive shells you’ll want to activate mise so tools and env vars load automatically as you cd around.
Add the appropriate line to your shell’s rc file:
| |
Restart your shell, then run a health check:
| |
For CI/CD, IDEs, and non-interactive scripts,
misealso supports shims instead of shell activation.
Concept 1: Dev tools
mise manages multiple versions of programming language runtimes and CLI tools on the same machine, then switches between them automatically based on the directory you’re in.
Tools are declared in the [tools] section of mise.toml:
| |
You rarely write this by hand. The mise use command adds tools for you and installs them:
| |
To run a tool once without installing it permanently, use mise exec (aliased to mise x):
| |
How version switching works
Once mise is activated, it walks up the directory tree looking for config files (mise.toml, .tool-versions, .node-version, etc.), resolves the requested versions, and prepends the correct binaries to your PATH:
| |
Move to another project pinned to a different version and node changes automatically. Tools come from multiple backends — core, aqua, npm, pipx, cargo, github, and asdf plugins — so the same mise use workflow installs almost anything. Browse the registry to see what’s available.
Concept 2: Environments
mise can load environment variables per project from the [env] section of mise.toml:
| |
When mise is activated, these are set automatically when you cd into the project and unset when you leave. You can also manage them from the CLI:
| |
A few handy features:
Unset a variable inherited from a parent config by setting it to
false:1 2[env] NODE_ENV = falseProvide a fallback that only applies if the variable isn’t already set, using
default:1 2[env] NODE_ENV = { default = "development" }Load a dotenv file with the
_.filedirective:1 2[env] _.file = '.env'Redact secrets so they don’t leak into task output:
1 2[env] SECRET = { value = "my_secret", redact = true }
Because env vars resolve alongside tools, you get a single source of truth for “what does this project need to run” — versions and configuration together.
Concept 3: Tasks
mise has a built-in task runner, so you can replace a Makefile or a pile of npm scripts. Tasks run with the full mise context loaded — your tools and env vars are already on PATH.
Define tasks in the [tasks] section:
| |
Run them with mise run (aliased to mise r):
| |
Task dependencies
The depends array is what turns a list of tasks into a build graph. When you run a task, mise first runs everything in its depends list, and because dependencies execute in parallel by default, independent steps don’t block each other:
| |
Running mise run ci executes lint and test at the same time, waits for both to finish, and only then runs ci’s own run command. There are related keys too — depends_post for cleanup steps that run afterwards, and wait_for for soft ordering without forcing a task to run. See task dependencies for the full list.
Some other highlights:
File watching with
mise watchto rerun a task when sources change.File tasks — instead of inlining shell into TOML strings, you can drop an executable script into a
mise-tasks/directory and get proper syntax highlighting and linting:1 2 3#!/usr/bin/env bash #MISE description="Build the CLI" go build ./...
Tasks also receive useful variables like MISE_PROJECT_ROOT and MISE_TASK_NAME, so scripts can be location-independent.
A runnable example
Let’s combine all three concepts into one small Go web project you can actually run. We’ll also add golangci-lint as an additional tool to show how mise manages more than just language runtimes.
1. Create the project
| |
2. Add the tools
Pin the exact Go version for this project. This creates mise.toml and installs Go if needed:
| |
Then add golangci-lint at a specific version. It lives in mise’s registry, so the same mise use workflow installs it:
| |
3. Configure environment and tasks
Open the generated mise.toml and make it look like this:
| |
The [tasks.ci] task uses depends to guarantee linting happens before the build — a small example of the task dependencies covered above.
4. Lock the tool versions with mise.lock
We set lockfile = true above, but lockfiles are not generated automatically. Run mise lock (or just mise install) to create one:
| |
This writes a mise.lock file next to mise.toml, pinning the exact resolved versions, checksums, and per-platform download URLs (truncated here for brevity):
| |
Commit mise.lock alongside mise.toml. Now even loose specs (like go = "1") resolve to the locked version on every machine, giving you reproducible installs. In CI you can go a step further with the locked setting (or mise install --locked) to fail fast if the lockfile is missing or incomplete.
5. Add the application code
Initialize the Go module:
| |
Create main.go:
| |
Note that the handler checks the error returned by fmt.Fprintf. This isn’t just good practice — golangci-lint’s default errcheck linter would otherwise flag the unchecked return value and the ci task would fail at the lint step.
6. Run it
| |
In another terminal:
| |
That’s the whole loop: mise.toml declares the tools (Go 1.26.4 plus golangci-lint 2.12.2), the environment (APP_NAME, APP_PORT), and the tasks (build, lint, serve, ci); mise.lock pins exactly what gets installed. Anyone who clones the repo and runs mise install followed by mise run ci gets the exact same setup — no README full of “first install the right Go version, then go install golangci-lint, then…” steps.
7. Clean up (optional)
| |
Bonus: global quality-of-life CLI tools
So far we’ve used mise for per-project toolchains, but it’s just as handy for the CLI tools you want available everywhere. The -g (--global) flag installs a tool and records it in your global config at ~/.config/mise/config.toml instead of a project mise.toml.
Here’s a starter kit of quality-of-life tools, each pulled from mise’s registry across different backends (core, aqua, npm, GitHub releases):
| |
Each command updates the same global config:
| |
Because this file lives in your dotfiles location, you can commit it to your dotfiles repo and reproduce your entire CLI toolbox on a new machine with a single command:
| |
A couple of tips:
- Run
mise lsto see everythingmisemanages (global and local) and which versions are active. - Pin a tool to a real version instead of
latest(e.g.mise use -g fzf@0.56.3) when you want reproducibility over always-newest. - Upgrade everything later with
mise upgrade.
Why this matters
The payoff is reproducibility with almost no ceremony. A single committed mise.toml answers three questions at once:
- Which tool versions does this project use?
- Which environment variables does it expect?
- How do I build, test, and run it?
New contributors run mise install and they’re ready — and because mise.lock pins exact versions, they get the same toolchain you do. CI runs the same mise run commands as your laptop. And you stop maintaining three separate tools to do one job.