mise
mise
Installation#
The official installer ships an optimized single binary, and it is the only method that supports
mise self-update:
It lands in ~/.local/bin/mise. Same command on macOS and Linux.
Windows:
Package-manager builds lag behind and cannot self-update. The Homebrew build refuses outright:
mise releases almost daily, and it talks to aqua, GitHub releases and language registries that keep
changing upstream — which is why the docs recommend staying on a recent version. Handing that
schedule to a formula maintainer is a poor trade. The winget build does allow self-update.
Shell Integration#
PowerShell supports both mise activate and the chpwd hook. The FAQ passage claiming native
Windows only works "via the use of shims until someone adds powershell support" is stale — the
shell compatibility table in the same docs lists mise activate as Yes for PowerShell. Only
[shell_alias] is genuinely unsupported.
Non-interactive shells need shims#
mise activate belongs in .zshrc, and .zshrc is never read non-interactively. ssh host 'command', LaunchAgents, cron and CI all take that path and would see none of the managed tools.
Put the shims directory in .zshenv to cover them:
Shims resolve the version for the current directory themselves, so per-project switching still works without activation:
That is strictly better than fnm's aliases/default/bin fallback, which is pinned to one version.
PATH order: installs first, shims after#
In interactive shells mise activate prepends ~/.local/share/mise/installs/*. Shims are only a
fallback and must sit behind them.
On macOS also mind path_helper: it runs from /etc/zprofile and moves the system paths to the
front, pushing anything set in .zshenv behind /usr/bin and even /opt/homebrew/bin. So the
shims need to be re-prepended in .zprofile as well:
Skip this and a future brew install node will silently shadow whatever version mise selected.
Where Versions Come From#
The global config lives at ~/.config/mise/config.toml (same path on Windows, ~\.config\mise\):
A mise.toml or .tool-versions in the project overrides it.
package.json fields are off by default#
.nvmrc, .node-version and the package.json fields are what mise calls idiomatic version files,
and they are all disabled by default. Enable them explicitly:
Once enabled, mise reads the packageManager and devEngines fields
() — enough to replace corepack for
per-project pnpm versions.
It does not read the traditional engines.node. That PR
() was never merged. fnm's --resolve-engines reads
exactly that field, so the capability is lost in the migration — projects relying on engines.node
need devEngines.runtime or a .node-version instead.
The GitHub API Rate Limit#
mise queries the GitHub Releases API to resolve versions, and the anonymous quota is only 60 requests per hour. Once exhausted, every install fails:
mise reads a token from the gh CLI's hosts.yml by default (github.gh_cli_tokens is true).
But when gh stores its token in the system keyring, hosts.yml holds nothing, so that path
yields no token:
Fetch it on demand instead — the token never touches disk or the environment:
It exists only for the instant mise needs it, which is a different thing from exporting a
credential into every child process.
Completions Need usage, in the Right Order#
mise's completion script shells out to the usage CLI at runtime. Homebrew pulls it in as a
dependency; the official installer and winget do not, so declare it:
There is a second trap: activate must come before the completions. usage is itself a
mise-managed tool, so before activation it is not on PATH, and every new shell prints:
Correct order:
On Windows that warning is more than cosmetic — with PowerShell as DefaultShell, any profile
output to stdout breaks scp/sftp:
Fix the order, the warning goes away, and scp works again immediately.
Three Layers of Supply-Chain Checks#
mise's npm: backend installs through the embedded aube, which brings three supply-chain
checks. Some tools get blocked by them — when that happens, do not reach for a global switch;
add the narrowest per-package exception instead.
The two ways it blocks an install#
1. Trust downgrade
An indirect dependency's older release carried trusted-publisher evidence while the newer one
does not, so aube treats it as a downgrade. Common causes are a manual publish, a backport that
skipped the trusted workflow, or a mirror that strips metadata — not necessarily tampering. The
AWS SDK's @smithy/* family behaves this way, and pinning one version at a time never ends, so
exempt the whole family by bare package name:
With a version it exempts only that version; a bare name exempts every version.
2. Build scripts denied
aube follows pnpm's build approval model: a dependency's preinstall / install / postinstall
does not run unless allowlisted (your own project's scripts still run). Packages that fetch a
platform binary in postinstall end up half-installed:
That message is badly misleading — nothing is wrong with the architecture. Those 479 bytes are
not a PE file at all but the author's "postinstall did not run" notice (header ec, not MZ).
Allow it:
allow_builds also takes an array to permit specific dependencies only: allow_builds = ["esbuild"].
Two escape hatches to avoid#
mise also offers npm.shell_out = true (use the npm CLI) and npm.package_manager = "pnpm".
Both bypass the trust policy — at the cost of dropping the checks for every package to unblock
one. The docs themselves label shell_out a last resort.
Note that npm.shell_out still passes --ignore-scripts to npm, so it does not even solve the
build-script problem.
The release-age gate#
The third layer is minimum_release_age, defaulting to 24h: only versions published longer
ago than the threshold are installed, giving the community time to catch a compromised release
(mirroring pnpm's minimumReleaseAge and Renovate's equivalent).
It gets in the way if you publish npm packages yourself — a version you just published cannot be installed for verification. Two ways around it:
This layer is independent of the other two — when a trust policy blocks an install, changing this setting has no effect whatsoever.
Migrating From fnm#
After uninstalling, clear the data directories: fnm keeps node under %APPDATA%\fnm (Windows) or
~/.local/share/fnm, and multishells under %LOCALAPPDATA%\fnm_multishells /
~/.local/state/fnm_multishells.
Check for running processes first:
A running node holds the exe inside node-versions open, and Remove-Item silently skips those
files, leaving a directory that will not delete. The multishells are junctions: removing the link
does not disturb already-running processes (their file handles stay valid), but those processes
will fail once they try to spawn a child.
On Windows the winget package needs a separate uninstall, and it cannot be done from an elevated session:
SSH sessions are elevated by default, so run winget uninstall --id Schniz.fnm --exact from a
normal local PowerShell.