At some point I noticed I had nvm, pyenv, a Go install in /usr/local, a hand-downloaded terraform binary, and direnv, each with its own shell hook. Every new machine meant an hour of setup.

mise handles all of that with one tool.

Install and activate

curl https://mise.run | sh

Then hook it into the shell. For fish:

# ~/.config/fish/config.fish
~/.local/bin/mise activate fish | source

For bash it’s eval "$(~/.local/bin/mise activate bash)".

Global defaults

mise use -g node@22 python@3.12 go@latest

This writes to ~/.config/mise/config.toml. Tools are downloaded on first use and live under ~/.local/share/mise, so nothing touches system packages.

Per-project versions

In a project directory:

mise use node@20 terraform@1.9

That creates a mise.toml you can commit:

[tools]
node = "20"
terraform = "1.9"

[env]
AWS_PROFILE = "dev"

cd into the directory and the right versions are on PATH, plus the environment variables, which is what I used direnv for. Teammates run mise install once.

It also reads existing .nvmrc and .python-version files, which made migrating old repos easy.

Tasks

The part I didn’t expect to use is tasks. Small projects that had a Makefile only for convenience now have this instead:

[tasks.build]
run = "go build -o bin/app ./cmd/app"

[tasks.test]
run = "go test ./..."

mise run test works the same on every machine.

Verdict

One binary, one config format, and a new machine is ready after mise install. I removed nvm, pyenv and direnv from my dotfiles and haven’t missed them.