Dotfiles Part 3: Unifying Polyglot Toolchains and AI Agent Environments with Mise
Say goodbye to the fragmented shims of nvm, pyenv, and rustup. Build a deterministic polyglot environment with Mise and Chezmoi, solving PATH drift for AI coding agents.
The previous two articles covered Dotbot’s symlink approach and Chezmoi’s template and secret management. That setup handles configuration versioning and distribution, but leaves runtime version management unresolved.
With Coding Agents working in day-to-day development, traditional version managers expose a critical failure mode: non-interactive shells drop environment variables. This article covers using Mise to manage polyglot environments and ensure terminal sessions and background agents run identical toolchains.
Problems with Polyglot Version Management
1. Shim Collisions and Startup Latency Across Tools
Setting up a polyglot development machine usually means installing multiple independent version managers:
- Node.js:
nvmorfnm - Python:
pyenv - Go:
gvmorgoenv - Rust:
rustup - Plus various CLI tools from Homebrew
To hook into commands, each tool appends initialization routines to ~/.zshrc:
eval "$(fnm env --use-on-cd)"eval "$(pyenv init --path)"eval "$(pyenv init -)". "$HOME/.cargo/env"export PATH="/usr/local/go/bin:$PATH"This causes two problems:
- Shell startup latency: Every new terminal tab runs multiple
evalstatements sequentially, adding hundreds of milliseconds of delay. - PATH priority chaos: Shims prepend themselves to
$PATH, causing system binaries to shadow version-managed tools or global packages to shadow local binaries.
2. Non-Interactive Shell PATH Drift
Commands like cargo build or pnpm test succeed in an interactive terminal, but background Coding Agents often fail with command not found or invoke outdated system versions.
The root cause is macOS Zsh startup file loading order:
Interactive login shell (new terminal tab):/etc/zshenv → ~/.zshenv → /etc/zprofile → ~/.zprofile → /etc/zshrc → ~/.zshrc → /etc/zlogin → ~/.zlogin
Non-interactive subshell (scripts, cron, agents running zsh -c):/etc/zshenv → ~/.zshenvMost developers put all version manager initialization in ~/.zshrc. When an agent spawns a non-interactive shell to run a build, the system never loads ~/.zshrc.
If the system falls back to macOS’s built-in Python 3.9, or fails to find Node 24 and Cargo configured in ~/.zshrc, the build fails.
Mise Architecture
Mise (formerly rtx) is a polyglot runtime manager and task runner written in Rust.
- Single binary: No shell wrapper layers, minimal execution overhead.
- Unified management: Supports Node, pnpm, Go, Rust, Python, Java, Ruby, and standalone GitHub Release binaries.
- Standardized configuration: Supports global
~/.config/mise/config.tomland projectmise.toml, compatible with.tool-versions. - Dual modes: Supports dynamic activation in interactive shells and static shim links in
~/.local/share/mise/shimsfor PATH integration.
Managing Global Mise Configuration with Chezmoi
Manage the global development environment declaratively. Track dot_config/mise/config.toml in Chezmoi:
[tools]actionlint = "latest"go = "latest"node = "24"pnpm = "12"rust = "latest"zizmor = "1.26.1"
[settings]legacy_version_file = trueRunning chezmoi apply distributes the configuration to ~/.config/mise/config.toml. Running mise install then installs any missing runtimes.
Adding Mise Shims to ~/.zshenv
To prevent PATH drift in background subprocesses and agents, put Mise’s shims in ~/.zshenv, which runs for every shell invocation.
In the Chezmoi template dot_zshenv.tmpl:
# .zshenv: environment variables only, loaded for every zsh invocation# Keep this file minimal, put heavy initialization in .zprofile or .zshrc
# 1. mise shims at the front of PATH:# Ensures Coding Agents, IDE tasks, and background subshells use declared tool versions# 2. Homebrew follows immediately:# Ensures modern CLI tools override Apple system defaultsexport PATH="$HOME/.local/share/mise/shims:/opt/homebrew/bin:/opt/homebrew/sbin:$PATH"Design Details
- Consistency across execution modes: Terminal commands and background Coding Agent commands resolve to the exact same
node,cargo, andpnpmpaths. - No activation overhead:
.zshenvavoidseval "$(mise activate zsh)". Static shims in~/.local/share/mise/shimsdispatch directly based on the current directory config. - Interactive features preserved: Interactive shells retain
eval "$(/opt/homebrew/bin/mise activate zsh)"at the end of~/.zshrcand~/.zprofilefor completions and shell hooks.
Verification
After applying the configuration, verify in both interactive and non-interactive shells:
# Verify interactive terminal resolutionwhich node cargo pnpm# Output: all point to ~/.local/share/mise/shims/...
# Verify background non-interactive invocationzsh -c "which node; which cargo"# Output: resolves to ~/.local/share/mise/shims/node and cargoRun mise reshim to refresh shim symlinks when needed.
When a project root contains mise.toml, entering the directory selects the project version. Without project configuration, the global defaults apply. Background processes resolve the intended runtime directly.
Next post: Taming Multi-Gigabyte Rust Targets: Global Build Caching with Mr Boxington (mbx) and Mise.