DEV Community

Cover image for Migrating from asdf and direnv to mise
Takashi Masuda
Takashi Masuda

Posted on • Originally published at masutaka.net

Migrating from asdf and direnv to mise

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.

GitHub logo jdx / mise

dev tools, env vars, task runner

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-version etc. requires configuration, described later)
  • Can define environment variables and task runners in mise.toml
  • Supports mise.local.toml for local settings meant to be gitignored. This allows individual adoption even before a team officially adopts mise
  • Well-organized tool management experience
    • mise install installs all tools from .tool-versions in one command
    • mise list shows 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

I also removed the direnv settings from ~/.zshrc.

-eval "$(direnv hook zsh)"
Enter fullscreen mode Exit fullscreen mode

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)"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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_OPTS via pre_asdf_install_ruby, but this turned out to be unnecessary with mise
  • ASDF_RUBY_BUILD_VERSION=master
  • Perl default-perl-modules
    • mise doesn't support default_packages_file for Perl (see the appendix below for a workaround)
  • direnvrc PATH_add

    • I had a configuration to automatically add ./bin to 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.toml when 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

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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.


  1. Hooks | mise-en-place 

Top comments (0)