
I made it so environment setup can be completed entirely with mise bootstrap
This page has been translated by machine translation. View original
Introduction
Hello, this is Shimada from the Manufacturing Business Technology Department at Classmethod.
I moved my environment setup over to the native bootstrap of mise.
mise is known as a runtime version management tool, but in July 2026, with v2026.7.4, mise bootstrap and mise dotfiles graduated from experimental. This makes it possible to declaratively write everything from installing system packages, symlinking dotfiles, and macOS system settings to changing the login shell in mise's configuration file.
This article introduces the steps I took to move an environment setup built with make all and shell scripts over to mise's native bootstrap. I also cover verification of whether GUI apps (Homebrew casks) can be installed with mise, and a comparison with Nix, which is also known for declarative environment management. This is a record of what I actually applied to my own dotfiles.
Note that these features have only just become GA. I confirmed the behavior in this article with mise 2026.7.7, but the behavior may change in future updates.
The previous setup
Before the migration, make all was the entry point, and it ran the shell scripts in .bin/ (installing Homebrew, symlinks, macOS settings, brew bundle) in order. Packages were written in .Brewfile, and macOS settings were written in a script that lined up defaults write commands.
In this setup, mise was used only to write runtime versions in ~/.config/mise/config.toml. The main players in environment setup were Homebrew and my own scripts, and mise was just one component of it.
mise's native bootstrap
mise bootstrap prepares a machine with a single command according to the current configuration. When run, it applies the following steps in order, declaratively and idempotently.
| Step | Config section | Description |
|---|---|---|
| packages | [bootstrap.packages] |
System packages (brew formula / brew-cask / mas / apt, etc.) |
| repos | [bootstrap.repos] |
Cloning git repositories |
| dotfiles | [dotfiles] |
symlink / copy / render into $HOME |
| defaults | [bootstrap.macos.defaults] |
macOS system settings |
| services | [bootstrap.macos.launchd.agents] etc. |
Background services |
| user | [bootstrap.user] |
Login shell |
| tools | [tools] |
Installing runtimes (mise install) |
| task | bootstrap task |
A place to offload auxiliary processing that cannot be expressed declaratively |
Each step has hooks before and after it, and processing that cannot be expressed declaratively can be offloaded to the final task named bootstrap. The division is: "declarative parts go in each section, procedural parts go in the task."
There is also a way to bundle shell scripts with mise's task runner, but for things like symlinks and macOS settings, where mise has declarative sections, moving them there makes the configuration shorter and easier to read.
Migration steps
Where to put the configuration
I placed the bootstrap configuration in mise.toml directly under the dotfiles repository.
The reason is to make the configuration readable even at the first run, when the [dotfiles] symlinks have not yet been created. ~/.config/mise/config.toml is the global configuration, but since it is itself created by a dotfiles symlink, the first bootstrap would depend on its own existence. With mise.toml directly under the repository, you can run mise bootstrap right after cloning the repository. Since mise.toml does not start with a dot, it is also not a target of symlinks.
System packages into [bootstrap.packages]
I moved the contents of .Brewfile into [bootstrap.packages]. Entries are in the format "manager:package-name" = "version".
[bootstrap.brew.taps]
"k1LoW/tap" = "https://github.com/k1LoW/homebrew-tap.git"
[bootstrap.packages]
"brew:ripgrep" = "latest" # CLI formula
"brew:k1LoW/tap/git-wt" = "latest" # formula with tap
"brew-cask:google-chrome" = "latest" # GUI app
"mas:539883307" = "latest" # Mac App Store (the number is the ADAM ID, LINE)
brew: is a formula, brew-cask: is a cask, and mas: is a Mac App Store app. For taps, you write the URL in [bootstrap.brew.taps].
Installed formulae can be extracted with mise bootstrap packages import, so the groundwork for the migration can be created with a command.
Can GUI apps (casks) be installed?
The biggest focus for completing environment setup with mise was GUI apps: whether casks like Chrome and Slack can be installed with mise.
In conclusion, you can declare them with brew-cask: and install them. Checking with mise bootstrap -n (dry-run), it resolves the actual version and assembles the installation steps as follows.
$ mise bootstrap -n
install cask google-chrome/150.0.7871.125
link app /Applications/Google Chrome.app
...
mas install 539883307 1429033973 497799835
As for the mechanism, mise's brew-cask backend fetches cask metadata directly from Homebrew's API (formulae.brew.sh). It resolves and installs casks without going through Homebrew's own subcommands.
I ran into a few pitfalls during the migration.
First, the package name must match the token in the API. What was written as cask "aws-vault" in the old .Brewfile was a formula, not a cask, in the API (brew:aws-vault is correct). Also, the current formula name for rustup-init is rustup. When moving declarations, you need to correct them to the official tokens in the API.
Next is state management of casks and mas. mise's brew-cask and mas backends manage installations using mise's own state. Apps that are already installed via Homebrew or the App Store are regarded as "not installed," and apply will try to reinstall them. This is not a problem on a brand-new machine, but when retrofitting onto a machine you are already using, it is safer to check the targets with mise bootstrap -n before applying.
Lastly, version pinning. This is not a property of mise but of Homebrew: for both formulae and casks, there is basically no mechanism to install an arbitrary older version later. The only things you can pin are those where a major version is published as a separate token (name@version), like postgresql@16 or node@20, and this is the only legitimate approach common to both formulae and casks. However, while such versioned tokens are abundant for formula language runtimes and databases, many GUI app casks distribute only the latest version. In my environment, the latest version of CleanShot X was outside the scope of my license, and brew-cask:cleanshot installed an unsupported version. Since CleanShot has no versioned token, I removed only this app from [bootstrap.packages] and, on the bootstrap task side, made it install a pinned, licensed version from the versioned DMG distributed by the official CDN. The design that lets you offload what can't be expressed declaratively to the final task paid off here as well.
dotfiles into [dotfiles]
I moved the symlinks I had been creating with my own scripts into [dotfiles]. For each target, you write the source and the mode. The source is a path relative to the repository root.
[dotfiles]
"~/.zshrc" = { source = ".zshrc", mode = "symlink" }
"~/.config" = { source = ".config", mode = "symlink" }
"~/.zsh" = { source = ".config/zsh", mode = "symlink" }
When I checked with mise bootstrap dotfiles apply -n after the migration, all existing symlinks matched the declarations.
$ mise bootstrap dotfiles apply -n
mise files: all files are applied
Note that only Claude Code's configuration directory (~/.claude) was removed from declarative management and left manual, because its current symlink structure is special and I was afraid of accidents. It is nice that there is an escape hatch to keep things outside rather than forcing what is hard to express declaratively into the declarations.
macOS settings into [bootstrap.macos.defaults]
I moved the script that lined up defaults write commands into [bootstrap.macos.defaults]. For each domain, you write keys and values. The type of the value (integer, boolean, string) determines the type written.
[bootstrap.macos.defaults]
"NSGlobalDomain" = { KeyRepeat = 2, InitialKeyRepeat = 15, AppleInterfaceStyle = "Dark" }
"com.apple.dock" = { autohide = true, tilesize = 59 }
Here too, a dry-run confirmed that everything matched the existing values.
$ mise bootstrap macos defaults apply -n
mise defaults: 16 value(s) already set
Login shell and the escape hatch
Changing the login shell (chsh in the old setup) can be written in [bootstrap.user].
[bootstrap.user]
login_shell = "/bin/zsh"
Auxiliary processing that cannot be expressed declaratively is offloaded to a task named bootstrap. In my case, installing uv and pnpm global packages fall under this. This task is automatically run at the end of mise bootstrap.
[tasks.bootstrap]
description = "uv と pnpm global パッケージを導入する"
run = [
"curl -LsSf https://astral.sh/uv/install.sh | sh",
"command -v pnpm >/dev/null 2>&1 && pnpm -g i || true",
]
One-time initial bootstrap
Running mise bootstrap requires mise and the repository itself. Since a brand-new machine has neither, I prepared just the minimal seed to resolve this chicken-and-egg problem in a single script.
Initially I had assumed the steps would be "install git, install ghq, fetch the repository with ghq, run install.sh." However, this can be shortened. ghq is a directory convention that places repositories at <root>/github.com/owner/repo, so if you simply git clone to that location, the ghq installed later will recognize it as is. There is no need to install ghq first. As for git, Homebrew's installer prepares it along with the Xcode Command Line Tools.
As a result, the first run fits into the following single command.
$ curl -fsSL https://raw.githubusercontent.com/reona5/dotfiles/main/.bin/install.sh | bash
This script installs Homebrew (git is installed here too), clones dotfiles to a path in the ghq layout, installs mise, and then runs mise bootstrap. The division is: re-setup on a machine that already has mise is just mise bootstrap, and the first run on a brand-new machine is this one command.
Also, for working repositories other than dotfiles, if you write their URLs in [bootstrap.repos], mise bootstrap will clone them all together. Only the dotfiles repository itself must be fetched first by this seed, since the configuration lives inside it.
Comparison with Nix
When it comes to declarative environment management, the predecessor is Nix. On macOS, by combining nix-darwin and home-manager, you can declaratively manage packages, dotfiles, macOS system.defaults, and even casks (via Homebrew). The scope that mise's native bootstrap now provides is something Nix has already achieved.
Let me organize the differences in character between the two.
| Aspect | mise bootstrap | Nix (nix-darwin + home-manager) |
|---|---|---|
| Positioning | A declarative layer that bundles existing tools | A purely functional package manager with its own store |
| Description | TOML | Nix language |
| Reproducibility | latest is not pinned and depends on Homebrew's current state |
Build reproducibility through hash pinning |
| Rollback | None (changes the system directly) | Atomic rollback through generation management |
| Handling of casks | Fetched from the Homebrew API | Delegated to Homebrew via homebrew.casks |
| Ease of adoption | Uses existing brew, etc. as is. Reversible | Requires installing the /nix store. High learning cost |
The key point is that mise is "a thin layer that declaratively bundles the tools I'm already using (Homebrew, App Store, shell)," whereas Nix is "a heavy foundation that takes on reproducibility and even rollback with its own mechanisms."
Nix is powerful if you want environment reproducibility and atomic rollback. On the other hand, the learning cost of the Nix language and flakes is high (I once tried to move to a Nix-based setup but gave up), and introducing the /nix store is a bigger intrusion into the environment. mise's native bootstrap does not guarantee reproducibility and rollback as strictly as Nix, but in exchange it lets you compose environment setup declaratively with just TOML and familiar tools. If you are already using mise for runtime management, the advantage is the seamlessness of extending it directly to environment setup.
Interestingly, both depend on Homebrew for handling casks. nix-darwin also delegates to Homebrew via homebrew.casks. This shows that installing GUI apps is still a domain that relies on Homebrew, even for declarative management tools.
Execution result
In the end, mise bootstrap -n now resolves through all the steps.
$ mise bootstrap -n
mise bootstrap: system packages
mise brew: 42 package(s) already installed
install cask google-chrome/150.0.7871.125
link app /Applications/Google Chrome.app
mas install 539883307 1429033973 497799835
mise bootstrap: dotfiles
mise files: all files are applied
mise bootstrap: system defaults
mise defaults: 16 value(s) already set
mise bootstrap: login shell
mise login_shell: already set to /bin/zsh
mise bootstrap: tools
mise all tools are installed
Dotfiles, macOS settings, the login shell, and runtimes match the current state, and casks and mas were resolved as installation steps.
Current achievements and limitations
To the question "can environment setup be completed with mise?", my impression is that this GA has almost achieved it. Everything from symlinks, macOS settings, system packages, casks, the Mac App Store, and the login shell can be written declaratively in a single mise.toml and applied with mise bootstrap.
There are three remaining limitations. First, because state management of casks and mas is mise's own, they are treated as reinstalls on machines where they are already installed. Second, running mise bootstrap requires mise itself, so the initial seed remains outside. Third, these have only just become GA, so some roughness remains for daily use. Being able to offload processing that cannot be expressed declaratively to the bootstrap task was genuinely helpful in compensating for this roughness.
Conclusion
I introduced the steps for moving environment setup to mise's native bootstrap.
Now that mise, which I had used for runtime management, can declaratively handle dotfiles and system settings as well, I was able to consolidate the entry point of environment setup into just mise bootstrap. While it does not have reproducibility on the level of Nix, I feel the appeal of this approach is the convenience of composing things declaratively with just TOML and familiar tools.