Assuming you’ve setup mise and have it configured in a couple of projects to switch between different versions of node, that will happen automatically when entering the project directories:
$ cd ~/src/proj1
$ node -v
20.0.0
$ cd ~/src/proj2
$ node -v
18.0.0
The way that mise is able to switch this version depends on whether you’ve activated mise with shims or not. In this article, I’ll explain how these methods work under the hood and the pros and cons of using them.
PATH Link to heading
The default behavior of mise activate
is using the “PATH” solution. This is relatively easy to understand,
though the actual logic gets quite complex. Basically every time the prompt is displayed, mise first
runs itself to check what the environment variables should be in that directory based on the .mise.toml
files.
If the current directory has the following .mise.toml
file:
[env]
FOO = "bar"
[tools]
node = "20"
python = "3.11"
Then mise will set PATH to include the following directories:
~/.local/share/mise/node/20/bin
~/.local/share/mise/python/3.11/bin
As well as set FOO to “bar”.
Shims Link to heading
Now let’s talk about shims. If you aren’t familiar with shims, I encourage you to first read my other article on the topic: Shims: Build a version manager.
The way mise works is a little different than what is described in that post though. Instead of creating
shims as bash scripts, mise shims are actually symlinks to the mise binary itself. You can see this
by listing the contents of the ~/.local/share/shims
directory:
$ ls -l ~/.local/share/shims/
lrwxr-xr-x node@ -> /opt/homebrew/bin/mise
lrwxr-xr-x npm@ -> /opt/homebrew/bin/mise
lrwxr-xr-x python@ -> /opt/homebrew/bin/mise
lrwxr-xr-x python3@ -> /opt/homebrew/bin/mise
lrwxr-xr-x python3.11@ -> /opt/homebrew/bin/mise
This is the same binary you run when you run commands like mise install
or mise use
. When mise
starts up, one of the first things it does is check to see the name it was called as. If it’s “mise”,
then it operates like normal. If it’s anything else, it operates in “shim” mode.
In shim mode, it will first load the mise environment. This means all of the env vars in .mise.toml
files as well as all of the current tools listed in those configs. If we assume our mise config
is the following:
[tools]
node = "20"
python = "3.11"
Then as part of that logic it will set PATH to include the following directories:
~/.local/share/mise/node/20/bin
~/.local/share/mise/python/3.11/bin
From there, mise simply execs the command that it was called as with all of the arguments. So
if you called the node
shim, with the -v
argument, it would be the same as running the following
but with the mise environment loaded:
$ node -v
However because we’ve modified PATH inside of the shim execution, node
won’t point to the mise shim and
end up in an endless loop, it will call ~/.local/share/mise/node/20/bin/node
instead.
mise x|exec
and mise r|run
Link to heading
You don’t need to use either of the above solutions with mise. If you don’t want to modify your shell’s
rc file, you can instead use mise x|exec
or mise r|run
to explicitly run something with mise.
mise exec
will load the mise environment just like it would be using the other methods (setting PATH and
other env vars in .mise.toml
files), and execute whatever you passed into it like the following:
$ mise exec -- node -v
20.0.0
mise r|run
is similar except it executes a task.
Sticking to mise exec
and mise run
is a nice way to keep your shell environment pristine and only
use mise when you explicitly want to.
Pros/Cons Link to heading
Now in general, I tell people they should use shims in non-interactive environments such as CI/CD, scripts, and IDE configuration. This is because the PATH solution doesn’t work in those environments at all. The prompt is never displayed there, so mise never has a change to update the environment.
However in interactive environments, I don’t recommend shims because it “breaks” which
.
If you run which node
, you’ll just get back the mise shim (e.g. ~/.local/share/shims/node@
)—which often isn’t that helpful. You can run mise which node
to get the path,
but it’s an extra step that’s easy to forget. In my workflows, I often use which
to find out what
tools I’m talking to so the PATH solution is a big help.
Another reason I don’t like shims is that it can only set arbitrary env vars when a shim is actually
called. For instance, if you set FOO=bar
in your .mise.toml
, then only when you call a shim like
node
will mise have the opportunity to set FOO. If you simply run echo $FOO
in your terminal,
mise won’t have set it because no shim was called. This is a problem for both interactive and non-interactive
environments.
This is one reason I added tasks to mise. Using tasks and mise run
, there is no need to have activated
mise and setup shims or anything else. Anytime you’re using mise run
you’ll already have the mise environment
fully setup.
Conclusion Link to heading
The way I suggest using mise is to use PATH for your local development and shims for IDE stuff. Things in scripts and CI/CD should use tasks. That way you’re really getting the best solution in each of those environments.