Writings · devlog
mise: The One Tool That Replaced Everything
One Rust binary, zero config files to maintain. The version manager, env loader, task runner, and secrets manager I finally consolidated into.
- Published
- Read
- 5m
Contents
I used to have a graveyard of version managers on my machine. fnm for Node. uv for Python. direnv for environment variables. corepack for pnpm. A Makefile for the scripts that didn't fit in package.json. Five tools, five config files, five things that broke at different times for different reasons.
Then I found mise. One Rust binary. All of it, gone.
What mise Actually Is
mise (formerly rtx) is a polyglot version manager, environment loader, task runner, and secrets manager — all in a single tool written in Rust. It's not a new wheel. It's what happens when someone looks at the toolchain a modern developer actually maintains and decides to put it under one roof.
The pitch is simple: asdf but fast, with env vars and tasks built in. If you've used asdf before, the mental model is identical. mise even reads asdf plugins natively — the migration is literally just installing mise and uninstalling asdf.
The Corepack Problem That Sold Me
I've been shipping TypeScript packages for years, and the package manager story has always been a mess. Do you use npm? pnpm? yarn? How do you make sure everyone on the team runs the same version?
Corepack was supposed to solve this. You'd run corepack enable, declare the package manager in package.json via the packageManager field, and hope everyone remembered to do the same thing. In practice, half the team forgot to enable corepack. The other half had stale versions. CI would fail with cryptic errors about mismatched package manager versions.
mise reads packageManager from package.json natively. No corepack enable. No global installs. You clone the repo, run pnpm install, and mise intercepts the command — checks the version in package.json, downloads it if needed, and runs it. Transparently.
# Clone a new project
git clone https://github.com/team/project.git
cd project
# Run any command — mise handles the rest
pnpm install
# mise: pnpm@9.15.0 → installed, usingThis is the kind of feature that sounds minor until you've debugged a CI failure caused by someone having pnpm 8.15.0 locally and CI running 8.12.0. The tool should just work. mise makes it work.
One Config to Rule Them All
The shift from five config files to one is the reason I keep using mise. Everything lives in mise.toml:
[tools]
node = "22"
python = "3.12"
go = "1.22"
terraform = "1.9"
pnpm = "9.15.0"
[env]
_.file = ".env"
DATABASE_URL = "postgresql://localhost:5432/myapp"
NODE_ENV = "development"
[tasks.build]
run = "npm run build && docker build -t myapp ."
depends_on = ["lint"]
[tasks.lint]
run = "npm run lint"
sources = ["src/**"]That's versions, environment variables, and tasks — all in one file. No Makefile. No .envrc. No package.json scripts hiding behind a wall of npm lifecycle hooks. One file, one place, one mental model.
The Secrets I Don't Put on Disk
The .env file has always bothered me. It's a plaintext file full of API keys sitting in the repo root (or worse, committed and then removed from history). For personal projects it's fine. For anything with a team, it's a liability.
mise integrates with 1Password and HashiCorp Vault directly:
[env]
STRIPE_API_KEY = "{{ exec(command='op read op://engineering/stripe/api_key') }}"
VAULT_TOKEN = "{{ exec(command='vault kv get -field=token secret/myapp') }}"The values get pulled from the vault at shell startup — never written to disk, never copied between machines. New developer joins the team? They get access to the 1Password vault, install mise, and everything loads on cd into the project. No shared secrets in Slack. No .env.example that drifts from reality.
The Task Runner I Didn't Know I Needed
I've always used Makefiles for cross-platform scripts. make lint, make test, make deploy. It works, but the syntax is hostile to newcomers, and every developer new to the project asks the same question: "what does make build actually do?"
mise tasks are self-documenting and dependency-aware:
[tasks.setup]
run = "mise install && npm install"
description = "Set up the full dev environment"
[tasks.dev]
run = "npm run dev"
depends_on = ["setup"]
[tasks.deploy]
run = "npm run build && wrangler deploy"
depends_on = ["lint", "test"]
sources = ["src/**"]Run mise tasks and you see a list of everything available with descriptions. Run mise run deploy and it checks dependencies, verifies sources haven't changed, and runs the pipeline. No more make --dry-run to figure out what's happening.
For monorepos, mise supports per-workspace tasks. Each sub-package can define its own tasks, and the parent orchestrates. It's the pattern I use now instead of npm workspace scripts.
What I Stopped Using
Here's what mise replaced in my actual setup:
| Before | After |
|---|---|
fnm + .nvmrc | [tools] node = "22" |
uv + .python-version | [tools] python = "3.12" |
direnv + .envrc | [env] in mise.toml |
corepack enable | Built-in packageManager reading |
Makefile | [tasks] in mise.toml |
.env files | 1Password/Vault integration |
asdf | mise (100% plugin compatible) |
Seven tools became one. The migration was the easiest tooling change I've made in years — because mise's plugin system is asdf-compatible, every .tool-versions file I had just worked.
The Speed Difference Matters More Than You'd Think
mise is written in Rust. That's not a marketing bullet — it changes how you use the tool.
asdf relies on bash shims. Every time you run node, the shim checks the .tool-versions file, resolves the correct version, and execs into it. There's a measurable delay on every command. On a fast machine it's maybe 50-100ms. On CI, it adds up across hundreds of shell invocations.
mise eliminates the shim overhead entirely for installed tools. Direct execution. The difference shows up in two places: terminal startup (no more that lag when you cd into a project) and CI pipelines (fewer seconds wasted on version resolution across dozens of install steps).
CalVer releases (YYYY.M.X) mean the tool ships frequently with clear expectations about recency. No more guessing whether a release is from last month or last year.
What I'm Still Figuring Out
CI/CD adoption is the one area I haven't fully resolved. Locally, mise is flawless. In a Docker-based CI pipeline, you're either baking mise into the image or installing it at the start of every job. The install overhead is small (it's a single static binary), but the caching strategy is different from what I had before. For projects where CI speed is critical, I need to benchmark whether mise or a lean Dockerfile with explicit version pinning wins.
Team migration is mostly painless, but there's a social cost. Every tool change requires a PR, a conversation, and a few people updating their local setup. The pitch that eliminates five tools in favor of one is compelling, but "compelling" and "actioned" are different things.
The Point
The best dev tool is the one you stop thinking about. mise doesn't make me faster at writing code — no tool does that, despite what the marketing says. What it does is remove the five minutes before I write code: the version check, the env load, the "did I enable corepack?" moment, the make setup that failed because someone changed a dependency.
I evaluate tooling by asking what happens in two years. Will mise still exist? The development velocity, the Rust foundation, the asdf compatibility layer, and the breadth of the feature set suggest it will. Is the config sustainable? One file, one schema, zero external dependencies for the config itself — yes.
If your development workflow involves juggling version managers, environment loaders, and task runners, mise is the consolidation point you've been looking for. It's not clever. It doesn't win any awards for originality. But my dev environment has never been this boring, and boring is exactly what infrastructure should be.