Skip to content
Go back

Mise Deep Dive: Tasks, Tools, Env Vars

By SumGuy 9 min read
Mise Deep Dive: Tasks, Tools, Env Vars

You Have Too Many Tools Managing Your Tools

You’ve got asdf for runtime versions. direnv for environment variables. A Makefile that hasn’t been touched since 2019 and breaks on macOS because of some GNU vs BSD flag nonsense. Maybe a .env file that gets sourced differently depending on who’s asking. And a scripts/ folder with shell scripts that only Dave knows how to run.

This is the status quo for most dev setups, and it’s genuinely exhausting.

Mise (pronounced “meez”, like mise en place) is a single tool that replaces all of that. It manages your language runtimes, your environment variables per-project, and your task runner — all from one config file. It’s a drop-in asdf replacement with a saner config format and a task system that doesn’t make you feel like you’re writing an ancient Makefile from memory.

As of mid-2026, mise is at v2025.x and has stabilized enough that you should absolutely be using it instead of whatever patchwork you have now.

Here’s how the three major features actually work, and how they fit together.


Part 1: Tool Version Management

This is what most people use mise for first, because it’s the obvious starting point. You want Node 22 in one project, Node 20 in another, Python 3.12 here, Go 1.22 there. Mise handles this via a .mise.toml file in your project root.

.mise.toml
[tools]
node = "22"
python = "3.12"
go = "1.22"

That’s it. Run mise install once and mise downloads and activates those versions. Switch directories and it automatically switches versions. No extra shell hooks to think about, no asdf reshim incantations.

Terminal window
$ mise install
mise [email protected] installed
mise [email protected] installed
mise [email protected] installed
$ node --version
v22.14.0

Mise uses shims by default but also supports a PATH-manipulation mode if you hate shims (you can set MISE_USE_SHIMS=false). The shim approach is generally fine and makes tool activation instant without requiring a shell restart.

Pinning Exact Versions

Fuzzy versions like "22" track the latest 22.x patch. That’s fine for most teams. For production parity where you care deeply about patch-level reproducibility, pin exact versions:

.mise.toml
[tools]
node = "22.14.0"
python = "3.12.9"

Run mise ls to see what’s installed and what’s active:

Terminal window
$ mise ls
Tool Version Source Requested
node 22.14.0 .mise.toml 22.14.0
python 3.12.9 .mise.toml 3.12.9
go 1.22.5 .mise.toml 1.22

Global Defaults

You can also set global tool defaults so you always have a working version everywhere, even outside a project directory:

Terminal window
$ mise use --global node@22
$ mise use --global [email protected]

This writes to ~/.config/mise/config.toml. Project-level .mise.toml always overrides it. Think of global as your “baseline so you’re never completely broken” and project-level as your “this is what everyone on this team needs to use.”

Non-Standard Tools

Mise isn’t just for language runtimes. It has a plugin ecosystem (compatible with asdf plugins) and first-party support for a wide range of tools:

.mise.toml
[tools]
node = "22"
terraform = "1.8"
kubectl = "1.30"
awscli = "2"
Terminal window
$ mise install
$ terraform --version
Terraform v1.8.5

This alone is worth the switch from asdf. Having your infra tooling versioned alongside your app tooling in the same file, committed to the repo, means “works on my machine” starts being a solvable problem instead of a vibe.


Part 2: Environment Variables

Here’s where it gets interesting and where a lot of people stop short with mise. Most people know about tool management. Fewer realize mise also replaces direnv for env var management.

Inside your .mise.toml, you can declare env vars at the project level:

.mise.toml
[tools]
node = "22"
[env]
APP_ENV = "development"
DATABASE_URL = "postgres://localhost:5432/myapp_dev"
LOG_LEVEL = "debug"

When you cd into the directory, mise activates those vars. When you leave, they’re gone. No source .env gymnastics. No “wait, is this var set from the global shell or the project?” confusion.

Terminal window
$ cd ~/projects/myapp
mise: loaded .mise.toml
$ echo $APP_ENV
development
$ cd ~
$ echo $APP_ENV
$

Secrets and .env Files

Checking secrets into .mise.toml is obviously not the move. Mise has a clean answer for this: load a separate .env file.

.mise.toml
[env]
_.file = ".env.local"
APP_ENV = "development"

.env.local gets read and merged in. You add .env.local to your .gitignore and commit .mise.toml with the safe vars. Your team clones the repo, copies a .env.local.example, and they’re set up without needing a Confluence page explaining which env vars go where.

.env.local
DATABASE_PASSWORD=supersecret
STRIPE_SECRET_KEY=sk_test_...
.gitignore
.env.local

Path Manipulation

One underused feature: mise can prepend to PATH from within the env config block.

.mise.toml
[env]
_.path = ["./bin", "./node_modules/.bin"]
APP_ENV = "development"

Now ./bin scripts and locally-installed Node binaries are on your path automatically when you’re in the project. This is what you were hacking around with npm run wrappers and shell aliases. Just… add it to the config.


Part 3: Tasks

This is the feature people sleep on most, and it’s genuinely good. Mise has a built-in task runner that replaces Makefiles for most use cases and works better on every OS than make does.

.mise.toml
[tasks.build]
description = "Build the app"
run = "npm run build"
[tasks.test]
description = "Run tests"
run = "npm test"
[tasks.dev]
description = "Start dev server with env"
run = "node server.js"

Run with mise run or the shorthand mise r:

Terminal window
$ mise run dev
mise: running task dev
node server.js

Tasks get tab-completion in your shell if you’ve got mise shell integration set up, which you should.

Multi-Step Tasks and Dependencies

Real tasks often need to run things in sequence. Mise handles this:

.mise.toml
[tasks.setup]
description = "Full project setup"
depends = ["install-deps", "migrate-db"]
[tasks."install-deps"]
run = "npm install"
[tasks."migrate-db"]
run = "npm run db:migrate"

mise run setup will run install-deps and migrate-db first, in parallel if there are no explicit ordering constraints between them. You can also use depends_post for teardown steps.

Multi-Line Commands

For anything more complex, you can write a full shell script inline:

.mise.toml
[tasks.ci]
description = "Run full CI pipeline locally"
run = """
set -e
npm install
npm run lint
npm test
npm run build
echo "CI passed locally"
"""

This is the thing Makefiles are awful at: readable multi-line scripts with real error handling. set -e means the whole thing stops on first failure. Try doing that cleanly in a Makefile without it turning into a research project.

File-Based Tasks

If your tasks get long enough that inline TOML feels unwieldy, you can put them in a mise-tasks/ directory as standalone scripts. Mise discovers them automatically:

mise-tasks/
build.sh
test.sh
deploy.sh
mise-tasks/deploy.sh
#!/usr/bin/env bash
set -e
echo "Deploying to $APP_ENV..."
rsync -avz dist/ user@server:/var/www/app/
echo "Done."

Make it executable and it shows up in mise tasks:

Terminal window
$ chmod +x mise-tasks/deploy.sh
$ mise tasks
Name Description
build Build the app
deploy (from mise-tasks/deploy.sh)
test Run tests

This is the sweet spot for most teams: simple tasks live inline in .mise.toml, complex tasks live as their own scripts that you can actually lint, test, and review properly.


Putting All Three Together

Here’s what a real-world .mise.toml looks like when you’re using all three features:

.mise.toml
[tools]
node = "22.14.0"
python = "3.12.9"
[env]
_.file = ".env.local"
_.path = ["./bin", "./node_modules/.bin"]
APP_ENV = "development"
DATABASE_URL = "postgres://localhost:5432/myapp_dev"
LOG_LEVEL = "debug"
NODE_ENV = "development"
[tasks.install]
description = "Install all dependencies"
run = "npm install && pip install -r requirements.txt"
[tasks.dev]
description = "Start development servers"
run = "npm run dev"
[tasks.test]
description = "Run full test suite"
run = """
set -e
npm test
python -m pytest tests/
"""
[tasks.lint]
description = "Lint all source files"
run = "npm run lint && ruff check ."
[tasks.build]
description = "Production build"
run = "npm run build"
[tasks.ci]
description = "Run CI pipeline locally"
depends = ["install", "lint", "test", "build"]

A new dev clones the repo, runs mise install to get the right tool versions, copies .env.local.example to .env.local, and they’re done. No README full of “first you need to install nvm, then pyenv, then…” instructions. No “oh you need to source that script first” slack messages at 9am.


The Edge Cases Worth Knowing

Shell integration matters. Mise needs a hook in your shell for the directory-change activation to work. Add this to your .zshrc or .bashrc:

Terminal window
$ eval "$(mise activate zsh)" # for zsh
$ eval "$(mise activate bash)" # for bash

Without it, you’d need to run mise shell explicitly in each new shell. Do it once, forget about it.

Trust prompts. The first time you enter a directory with a .mise.toml, mise asks if you trust it. This is a security feature, not a bug. Run mise trust to approve a directory, or mise trust --all if you want to stop seeing prompts globally (not recommended for shared machines).

Existing asdf users. Mise reads your existing .tool-versions files. Migration is genuinely zero-effort. Point mise at the same directory, it picks up your existing config. You can switch back and forth while you’re evaluating.

CI/CD. Mise works fine in GitHub Actions and GitLab CI. Use mise install --non-interactive in pipelines to skip the trust prompt. Some teams commit a .mise.toml with CI-specific tool pins and let the runner install fresh each time. Others pre-bake a Docker image with mise + tools installed and just run tasks. Either works.


Should You Bother?

If you’re using asdf, honestly yes. The config format is nicer, the tool is faster, the task runner is legitimately good, and the env var handling means you can retire direnv. That’s three tools consolidated into one.

If you’re not using any runtime version manager yet, mise is the obvious starting point in 2026. It’s the kind of tool you install, configure once, and then it just works until you remember it exists three months later when a coworker asks why your node --version is different from theirs.

If you’re on a team still using raw Makefiles, introduce this with the task runner angle first. That’s the easiest sell. Once people are using mise run test and getting proper error messages, the tool management part is a natural follow-on.

The setup is twenty minutes. The benefit is not having to explain your local dev environment ever again.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Boundary vs Teleport

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts