All articles
14 Oct 2026 / 6 min read

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.

# mise# devops# ci
A toolbox connecting multiple type of machines

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-versions if 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:

toml
[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:

bash
mise install            # download the tools, at the right versions
mise tasks              # what can be done here
mise run test-unit

That's it. No README saying "install these ten things, in these versions, in this order".

Locally

One line in your shell configuration:

bash
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:

yaml
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 lint

The 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:

bash
curl https://mise.run | sh

Clone 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:

yaml
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 tasks tells 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 clone and mise 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. ๐Ÿ™‚