mise, in every repository
In 2021 I wrote about asdf + direnv. Today I use mise: one tool for versions, variables and tasks, the same on the laptop, in CI and on bastions. And it belongs in every repository.

In 2021 I wrote an article about asdf + direnv: how to get the right version of every command-line tool, folder by folder, without losing your mind.
The problem hasn't changed. One project wants kubectl 1.35, the one next to it 1.31, CI has a third one, and the bastion has whatever someone installed by hand two years ago.
The tool has changed, though: today I use mise. And I think it belongs in every repository.
What it is
mise is a single binary doing three jobs:
- tool versions: node, go, kubectl, opentofu, each at the version the project asks for (what asdf did);
- environment variables per folder: step in and they're there, step out and they're gone (what direnv did);
- tasks: the project's commands, with a name and a description (what usually ended up in a Makefile).
It all lives in one file, mise.toml, at the root of the repository.
Why instead of asdf + direnv
asdf + direnv worked, but it was two tools, two files (.tool-versions and .envrc) and one plugin to install for every tool. On top of that, asdf goes through a shim for every command: each kubectl you run is first a script deciding which kubectl to run.
mise does the same things on its own:
- one file instead of two, and it still reads a
.tool-versionsif it finds one, so switching is painless; - no shims by default: when you enter a folder it updates your
PATH, and commands run directly; - besides asdf plugins, it can fetch tools straight from GitHub releases, Go packages, npm, pipx and cargo, with no plugins to write or maintain;
- it's fast, to the point you don't even notice it.
A real mise.toml
In the GitHub organization of the SIGHUP Distribution by ReeVo, which I contribute to, mise is everywhere: there's a mise.toml in almost every repository, from furyctl to the distribution modules. A colleague of mine brought it in, and we never looked back.
This is a piece of the one in furyctl, trimmed down:
[tools]
go = "1.26.5"
golangci-lint = "2.12.2"
goreleaser = "2.15.4"
# Tools taken from a GitHub release and a Go package, no plugins
"ubi:mvdan/gofumpt" = "v0.6.0"
"go:github.com/onsi/ginkgo/v2/ginkgo" = "v2.19.0"
[env]
CGO_ENABLED = 0
[tasks.lint]
description = "run linting with golangci-lint"
run = "golangci-lint -v run --timeout=10m --config=.rules/.golangci.yml ./..."
[tasks.test-unit]
description = "run unit tests"
run = "go test -v -tags=unit ./..."
[tasks.build]
description = "build with goreleaser (snapshot)"
run = "goreleaser release --verbose --snapshot --clean"Whoever clones the repository runs:
mise install # download the tools, at the right versions
mise tasks # what can be done here
mise run test-unitThat's it. No README saying "install these ten things, in these versions, in this order".
Locally
One line in your shell configuration:
eval "$(mise activate zsh)"From then on you enter a folder and get that project's tools, enter another and get its own. This site works the same way: a mise.toml with node and pnpm, and tasks for development, build and validation.
In CI
This is where mise shines: the pipeline uses the same tools, at the same versions, and runs the same tasks I run on my laptop. If mise run test-unit passes for me, CI does exactly the same thing.
There we use Drone, with an image that just has mise preinstalled. The first step installs the tools, the others use them. This is furyctl's pipeline, trimmed down:
volumes:
- name: mise-cache
host:
path: /root/mise_data_dir
steps:
- name: install-tools
image: quay.io/sighup/mise:v2026.6.14
environment:
MISE_DATA_DIR: /mise-data
MISE_GITHUB_TOKEN:
from_secret: GITHUB_TOKEN
volumes:
- name: mise-cache
path: /mise-data
commands:
- mise install
- name: lint
image: quay.io/sighup/mise:v2026.6.14
depends_on: [install-tools]
environment:
MISE_DATA_DIR: /mise-data
volumes:
- name: mise-cache
path: /mise-data
commands:
- eval "$(mise activate bash --shims)"
- mise run lintThe volume and the token aren't there by chance: more on them in a moment. On GitHub Actions there's also a ready-made action, jdx/mise-action, that does it all in one step.
On bastions
On remote bastions you often aren't root, and what's installed is what's installed. mise installs in your home directory, without root, with one command:
curl https://mise.run | shClone the repository, mise install, and you have the same kubectl, furyctl and opentofu as on your laptop. No more "the bastion has an old version, so the command is different there".
Where to be careful
mise isn't magic, and we learned a couple of things the hard way.
GitHub limits. Many tools come from GitHub releases, and mise also uses the GitHub API to find out which versions exist. Without authentication the limit is low: in CI, or from a bastion sharing its outgoing IP with many others, you hit it quickly and the install fails. The fix is giving mise a GitHub token, in MISE_GITHUB_TOKEN or GITHUB_TOKEN.
The cache in CI. Without some care every job starts from scratch and downloads everything on every run: go, kubectl, python, and so on. A volume shared between jobs for mise's folder (MISE_DATA_DIR) is worth it, so runs using the same tools find them already there. But if several pipelines run in parallel on the same worker, two mise install writing to the same folder step on each other's toes. In other repositories of the organization the install goes through a flock, so whoever comes second waits their turn:
commands:
- "(flock -w 1800 9 || exit 1; mise install) 9>/mise-data/.mise-install.lock"A standard, in every repository
That's why I think mise belongs in every repository, even the small ones:
- "works on my machine" stops being an excuse: versions are written in the repository, the same for everyone;
- documentation runs:
mise taskstells you what can be done, and tasks are the real commands, not a README going stale; - CI matches the laptop: same tools, same commands;
- you start in two commands:
git cloneandmise install; - and it helps me too, six months from now, when I'll have forgotten everything.
It's also one of the first things I look at when I open a repository to tell whether it's well kept, as I said in Who presses the button.
Goodbye asdf, goodbye direnv: thanks for everything. ๐