I first tried mise while setting up Arch Linux on my Dell XPS 13. I was bringing over my Doom Emacs configuration, which used asdf.el, and considering how mise would fit into the same environment.

Colleagues had already spoken well of it. What interested me was the possibility of simplifying runtime integration in both Fish and Emacs. If that worked on the Dell, I was considering the same change on my Mac.

The useful test was inside the editor. I set up separate directories for Ruby 3.4 and 3.3, opened their Ruby files in Doom, and checked the version from each buffer. Each one used the Ruby selected for its directory. I also confirmed that Doom could find Node and npm, and that the Elixir tools were available.

That gave me a working setup on Arch before moving my main Mac environment away from asdf. The Mac work then extended into Fish configuration and the tools Emacs launches. The migration described here took place in October 2026; the Mac checks used mise 2026.10.21.

Keeping project version files

The starting point on the Mac was a set of projects with existing version files. Replacing the manager did not require changing every project’s Ruby or converting all those files to a new format.

Mise reads .tool-versions, the format I was already using with asdf. For ecosystem-specific files such as .ruby-version and .nvmrc, I enabled the corresponding tools in the global settings:

[settings]
idiomatic_version_file_enable_tools = ["ruby", "node"]
experimental = true

The experimental setting is used by the installed mise.el, which checks it before enabling the integration.

My personal defaults now live in ~/.config/mise/config.toml. Its current tool selections include changes made during the wider cleanup:

[tools]
elixir = "1.20.2-otp-29"
go = "1.27"
ruby = "4.0.9"
rust = "1.99.1"

Those are personal defaults. A project’s files can select another version. During the migration checks in the blog repository, mise current ruby reported 3.4.6. The project’s .tool-versions selected that version even though its .ruby-version said 3.4.5.

The configuration rules determine how global and project files combine. I checked the effective selection rather than assuming that finding a familiar version file was enough. mise config ls lists the configuration files being read, and mise current ruby reports the selected Ruby.

I manage my dotfiles with GNU Stow. During the transition, the package still named asdf retained .tool-versions, .default-gems and .default-mix-commands. I later moved those files into the mise package and recreated their Stow links. The move left the effective runtime selections unchanged. That was configuration housekeeping, separate from moving or installing runtimes.

The default-installation files were retained, but I have not validated that a fresh Ruby or Elixir installation consumes them automatically.

Letting Fish activate mise

On Arch, the first mise doctor check reported that mise was not activated. After setting up Fish activation, it reported no problem. Installing the executable had been one step; arranging the shell environment was another.

On the Mac, mise is installed through Homebrew. I did not need to add an activation line to my own config.fish: this installation supplies /opt/homebrew/share/fish/vendor_conf.d/mise-activate.fish:

if [ "$MISE_FISH_AUTO_ACTIVATE" != "0" ]
  /opt/homebrew/opt/mise/bin/mise activate fish | source
end

The Fish setup documentation describes this package-provided activation. I kept that mechanism instead of adding another copy to my personal configuration.

The surrounding Fish changes made the command paths explicit. Personal paths moved into conf.d/00-environment.fish, which normalizes the inherited PATH, removes duplicates and filters known obsolete entries, including the old asdf shims. Asdf and older installations were cleaned up as part of that work.

The prompt also needed to follow the new manager. My old override parsed .tool-versions itself. Its replacement asks mise current once per render and displays the languages relevant to the directory. Ruby, Node and Elixir were joined by Rust, detected through files such as Cargo.toml. The prompt now uses mise’s selection rather than maintaining its own version-file parser.

Rustup remains in this setup: the mise Rust entry links to the existing ~/.cargo/bin directory. I added Rust to the same prompt segment rather than adding another standalone version check.

The wider Fish review also removed the old Oh My Fish Emacs plugin and kept the command helpers I use as autoloaded functions. That removed an emacs --version call from shell initialization. I did not measure a before-and-after startup time for it.

The environment inside Doom

The Doom change had two parts. I replaced the asdf.el package declaration with the recipe for mise.el in packages.el:

(package! mise :recipe (:host github :repo "eki3z/mise.el"))

In config.org, I replaced the asdf-enable setup with:

(use-package! mise
  :config
  (global-mise-mode +1))

On Arch, after synchronizing Doom, I confirmed that the mode was active and that Emacs could find the Ruby shim. I checked Ruby LSP and ElixirLS in that setup.

The installed revision of mise.el makes the mechanism visible. It calls mise env --json, merges the result into a buffer-local process-environment, and derives a buffer-local exec-path from the resulting PATH.

Emacs uses exec-path to find a program named without an absolute path. The process it starts inherits process-environment, including the PATH used to find its own child commands. The Emacs process reference describes both roles. That is why checking Ruby in a terminal alone could not validate what a formatter or language server launched by Emacs used.

The package uses inheritenv around selected commands, including shell-command-to-string, so they can carry the editing buffer’s environment into temporary buffers.

Doom has its own environment snapshot as well. In this revision, doom sync regenerates an existing snapshot; doom sync --env also creates one if absent. The Emacs command doom/reload-env only loads the saved file into the running session. On Arch, doom doctor continued to warn that my PATH had changed even while the runtime checks worked. I did not establish the cause of that warning.

On the Mac, my Ruby formatter configuration also follows the project: RuboCop when a .rubocop.yml is present, otherwise Standard. RuboCop uses bundle exec when there is a Gemfile. Both commands clear inherited Bundler-related variables before starting, and Standard uses the gems available to the selected Ruby. The full configuration contains the details; the important connection here is that a formatter needs the project’s environment as well as its configuration file.

Following the development tools

Some settings outside the runtime manager still named the old locations. My Arduino configuration was one of them:

 (defvar bounga/arduino-language-server
-  (expand-file-name "~/go/bin/arduino-language-server"))
+  (executable-find "arduino-language-server"))

The defvar initializer does not repeat the lookup each time I open an Arduino buffer. Resolving it to a mise shim gives that saved value a stable path while the executable lives under a particular Go installation. Checking the running Emacs confirmed the shim path, and the server answered its help command.

An installation problem with arduino-language-server and dockfmt led me to check Go. Mise had Go 1.27.3 installed, but it was inactive in the directory where I was working, so ordinary commands still found Homebrew’s Go 1.26.6. mise exec go@1.27.3 -- go version used the intended installation, and both tools built with it in a temporary directory. I then selected Go globally and installed them.

mise install does not record a version selection; mise use records one and installs the runtime if needed. I did not reproduce the original installation error, so the inactive Go is an observed problem, not a demonstrated cause of that failure.

This setup puts Go-installed executables in the selected Go version’s bin directory through GOBIN. It changes where the tools live. After adding an executable there, mise reshim makes it available through the shim directory.

That mattered for dockerfmt, which Doom actually uses for Dockerfiles. I had a copy installed with Homebrew, and Emacs kept finding it after I installed dockerfmt with the mise Go. After mise reshim, executable-find resolved to the mise shim. The earlier dockfmt build concerned a different formatter. Doom’s Docker configuration uses dockerfmt, although its doctor check still looks for dockfmt.

Gems outside the checkout

I made a separate storage change during the cleanup: removing the global Bundler path setting that sent gems into vendor/bundle. Projects using the same Ruby installation can now share installed gems.

Mise selects Ruby and, in this setup, its RubyGems directory. Each project’s Gemfile and lockfile still select its dependencies. With Ruby 3.4.6, Gem.dir matches Bundler.bundle_path under both Bundler 2.3.17 for the blog and 4.0.17 for ctf_tracker.

That follows the Bundler policy, not a mise feature. The configuration reference specifies Gem.dir as the default before Bundler 5, and a repository-relative .bundle directory starting with Bundler 5. A project can override path.

Seven active projects passed dependency and principal-library load checks against the shared directories, with their Gemfiles, lockfiles and Ruby selection files unchanged. After cleanup, ctf_tracker also passed its 86 examples. The global automatic-installation and cleanup settings were removed, along with a production exclusion that excluded no dependencies in those projects. My .gemrc now only disables documentation and backtraces, with its Stow link preserved.

The 45 removed vendor/bundle trees had an allocated size of about 10.46 GiB in the retained du -sk inventories. Gems were also installed in the shared directories, so this is not a measured net saving. With those trees removed from the checkouts, I re-enabled LSP file watchers. After restarting Emacs, a large Rails project opened without a warning or noticeable slowness, an observation limited to that project.

The two Ruby buffers on Arch were an early check that per-directory selection could work inside the editor. On the Mac, getting there also meant replacing old paths and checking the tools launched by the editor. The project dependency files could stay as they were while I changed the runtime manager and the environment around them.

Comments, questions, or just a reaction?

Send an email to ~bounga/bounga.org-discuss@lists.sr.ht. It is public and archived, so other readers can follow along and answer too.