For managing versions of development tools like Ruby and Node.js, I had gone through *env tools like rbenv
and nodenv
, then switched to asdf
in 2019. For environment variable management, I had been using direnv
since even earlier—2014.
Recently, a tool called mise has been gaining attention. I wasn’t particularly having issues, but out of curiosity and the motivation to reduce the number of tools—since I heard mise also has direnv-like functionality—I decided to make the switch. My environment is macOS.
What is mise?
mise (pronounced “meez”) is a tool that handles both development tool version management and environment variable management in one place. It provides asdf-compatible runtime management along with direnv-equivalent environment variable management.
Here are the notable features I found after using it:
- Can read
.tool-versions(.ruby-versionetc. requires configuration, described later) - Can define environment variables and task runners in
mise.toml- The official mise.toml may be a good reference
- Supports
mise.local.tomlfor local settings meant to be gitignored. This allows individual adoption even before a team officially adopts mise - Well-organized tool management experience
mise installinstalls all tools from.tool-versionsin one commandmise listshows all tool versions and their source files at a glance
What I Did for the Migration
The official documentation has a migration guide from asdf , so start there. Below are the specific steps for my environment.
1. Homebrew Package Changes
I uninstalled asdf and direnv, and installed mise.
$ brew uninstall --force asdf
$ brew uninstall --force direnv
$ brew install mise
2. zsh Configuration Changes
I removed all asdf settings from ~/.zshenv.
-MY_ASDF_CONFIG_HOME="${XDG_CONFIG_HOME}/asdf"
-export ASDF_CONFIG_FILE="${MY_ASDF_CONFIG_HOME}/asdfrc"
-export ASDF_DATA_DIR="${XDG_DATA_HOME}/asdf"
-export ASDF_GEM_DEFAULT_PACKAGES_FILE="${MY_ASDF_CONFIG_HOME}/default-gems"
-export ASDF_NPM_DEFAULT_PACKAGES_FILE="${MY_ASDF_CONFIG_HOME}/default-npm-packages"
-export ASDF_PERL_DEFAULT_PACKAGES_FILE="${MY_ASDF_CONFIG_HOME}/default-perl-modules"
-export ASDF_PYTHON_DEFAULT_PACKAGES_FILE="${MY_ASDF_CONFIG_HOME}/default-python-packages"
-export ASDF_RUBY_BUILD_VERSION=master
-PATH=$ASDF_DATA_DIR/shims:$PATH
I also removed the direnv settings from ~/.zshrc.
-eval "$(direnv hook zsh)"
Instead, I added the mise configuration to ~/.zshrc. Since the activation mechanism enabled by this setting handles PATH management, no configuration in ~/.zshenv is needed.
eval "$(mise activate zsh)"
Here are the actual files:
With the asdf-era environment variables gone, my zsh configuration became cleaner.
3. Installing Tools
I reinstalled the tools that asdf had been managing using mise install.
$ cat ~/.tool-versions
nodejs 24.14.0
ruby 4.0.2
$ mise install
$ mise list
Tool Version Source Requested
node 24.14.0 ~/.tool-versions 24.14.0
ruby 4.0.2 ~/.tool-versions 4.0.2
4. Migrating default_packages_file
With asdf, I managed default_packages_file via environment variables in ~/.zshenv. With mise, I consolidated them in ~/.config/mise/config.toml.
[settings.node]
default_packages_file = "~/.config/mise/default-npm-packages"
[settings.ruby]
default_packages_file = "~/.config/mise/default-gems"
During the migration, I discovered a bug where ~ in Node.js’s default_packages_file wasn’t being expanded, causing default packages not to be installed. I reported it in Discussion#8606
, and it was fixed in PR #8709
🙏 This is resolved in v2026.3.11
and later.
5. Configuring .ruby-version and .node-version Support
Neither asdf nor mise reads .ruby-version or .node-version by default. With asdf, you set legacy_version_file = yes in asdfrc, but with mise, the following configuration is needed. I added it to the repository’s mise.toml or mise.local.toml.
[settings]
idiomatic_version_file_enable_tools = ["node", "ruby"]
What I Didn’t Migrate
Some settings were either unnecessary to migrate or I chose not to migrate.
- Ruby pre-install hook
- With asdf, I set
RUBY_CONFIGURE_OPTSviapre_asdf_install_ruby, but this turned out to be unnecessary with mise
- With asdf, I set
ASDF_RUBY_BUILD_VERSION=master- mise automatically fetches the latest version of ruby-build when installing Ruby, so this setting is unnecessary
- Perl default-perl-modules
- mise doesn’t support
default_packages_filefor Perl (see the appendix below for a workaround)
- mise doesn’t support
- direnvrc PATH_add
-
I had a configuration to automatically add
./binto PATH for Ruby projects with a Gemfile.lock -
mise doesn’t have an equivalent global feature, but I decided to configure it per-project in
mise.tomlwhen needed[env] _.path = "./bin"
-
Conclusion
I replaced two tools—asdf + direnv—with just mise. The zsh configuration became cleaner, and being able to consolidate tool versions and environment variables in mise.toml is a nice benefit.
There were some gotchas like the Emacs compatibility issue described in the appendix below, but overall I’m happy with the migration.
Also, with direnv no longer needed, I was able to migrate from .envrc to .env, which later enabled me to set up .env mounting with 1Password Environments
.
References
- mise Getting Started
- miseは便利: タスクランナー兼ツールバージョン管理&環境変数管理ツール - TechRacho
- 1Password Environments
- 1Password Environmentsで.envファイルを管理できるようになったので試してみた - DevelopersIO
Appendix
Fixing ruby-lsp Not Working in Emacs
I encountered an issue where ruby-lsp wouldn’t work in Emacs for repositories using a Ruby version different from the global one.
mise activate zsh takes a hook-based approach, using zsh’s chpwd hook to switch tool versions when changing directories. However, Emacs doesn’t execute this hook, so mise’s version switching doesn’t work.
mise offers an alternative to hook-based activation called shims.
| activate | shims | |
|---|---|---|
| Best for | Interactive shell | Non-interactive (IDE, Emacs, scripts) |
Hooks like cd |
Triggered | Not triggered |
which result |
Returns the actual tool path | Returns the shim path |
Initially, I added eval "$(mise activate zsh --shims)" to ~/.zshrc to enable both, but the mise documentation
assumes activate and shims are mutually exclusive.
I ultimately settled on the following setup:
eval "$(mise activate zsh)"in~/.zshrc(for interactive shell)- Add the shims directory to PATH only when launching Emacs
alias emacs="LC_COLLATE=C PATH='${XDG_DATA_HOME}/mise/shims:$PATH' emacs"
This creates a separation of concerns: regular zsh sessions use hooks only, while Emacs uses shims.
Compared to asdf’s simpler shims-only design, this setup might look more complex at first glance. However, I understand this is a tradeoff stemming from mise being a replacement for both asdf and direnv. The real-time reflection of environment variable changes in the interactive shell via mise activate is something shims alone cannot achieve.
Workaround for Perl default-packages
mise’s default-packages mechanism is a language-specific feature hardcoded in core plugins (Go, Node.js, Python, Ruby). Perl is not a core plugin—it’s installed via the Aqua backend—so it’s not covered.
As a workaround, you can run cpanm in mise.toml’s [hooks] postinstall1. This way, mise install alone sets up both Perl and CPAN modules.
[settings]
experimental = true
[hooks]
postinstall = "cpanm CGI HTML::Template"
Note that hooks is an experimental feature, so experimental = true is required. Also, postinstall runs on every tool installation, not just Perl, so this hook will be triggered every time you run mise install.
The Relationship Between mise and aqua
aqua
is a tool specialized in version management for CLI tools (terraform, gh, etc.), with robust security through checksum verification. While mise and aqua cover different scopes, mise can use aqua-registry as a backend (via the aqua: prefix), allowing you to install tools managed by aqua through mise.
They’re not identical, but for personal dotfiles management, I feel mise alone can sufficiently cover aqua-like use cases. If your team uses aqua or you need checksum verification for supply chain security in CI/CD, there’s still value in using aqua separately.