docs/bootstrap.md
mise bootstrap sets up a machine for the current config in one command: Linux
users and groups, OS packages, privileged files and directories, system services,
Linux host firewall policy, Docker Compose projects, git repos, dotfiles, mise shell
activation, macOS defaults, macOS LaunchAgents, Linux systemd user services,
the user's login shell, tools, and any final project-specific task. It can
consume declared secret inputs without storing their values in mise config. You
can also add hooks that run at named points in the bootstrap sequence.
The same configuration can be applied to named inventory hosts or ad-hoc SSH
destinations with mise bootstrap remote.
Use bootstrap for things that are needed before a project or workstation is
ready, but that do not belong in [tools]: native libraries, Homebrew
formulae, dotfile repositories, shell rc files, editor config, macOS
preferences, user services, and one-time machine setup.
mise bootstrap runs these steps in order:
Before making changes, mise resolves any required
[bootstrap.secrets] used by the files phase. This
preflight prevents a missing input from leaving a partially provisioned host.
mise bootstrap accounts apply converges
[bootstrap.users] and [bootstrap.groups].mise bootstrap plugins apply installs package manager plugins declared in
[bootstrap.plugins].[bootstrap.packages].mise bootstrap files apply converges
[bootstrap.files] and [bootstrap.directories].mise bootstrap services apply converges existing systemd system units from
[bootstrap.services].mise bootstrap firewall apply converges host firewall policy and rules from
[bootstrap.linux.firewall].mise bootstrap compose apply converges
[bootstrap.compose] projects.mise bootstrap repos apply clones or updates
[bootstrap.repos].mise bootstrap dotfiles apply applies [dotfiles].mise bootstrap mise-shell-activate apply configures shell activation from
[bootstrap.mise_shell_activate].mise bootstrap macos defaults apply writes
[bootstrap.macos.defaults].mise bootstrap macos launchd-agents apply writes and loads
[bootstrap.macos.launchd.agents].mise bootstrap linux systemd-units apply converges
[bootstrap.linux.systemd.units]
by writing unit files, enabling/disabling them, and starting/stopping them
as configured.mise bootstrap user apply applies [bootstrap.user].mise install installs missing [tools].mise run bootstrap runs a task named bootstrap, if one exists.[bootstrap.hooks.final] runs after the bootstrap task, if configured.Use mise bootstrap --skip <part> to skip specific parts. Supported parts are
accounts, plugins, packages, files, services, firewall, compose, repos, dotfiles, mise-shell-activate,
macos-defaults, macos-launchd-agents, linux-systemd-units, user, tools,
task, and final-hook. The old shorter names shell, defaults, launchd,
and systemd are still accepted as aliases. The flag can be repeated or
comma-separated, for example mise bootstrap --skip tools,task.
Use mise bootstrap --only <part> to run only specific parts. It supports the
same part names and can be repeated or comma-separated, for example
mise bootstrap --only dotfiles,tools. --only and --skip are mutually
exclusive.
Use mise bootstrap --update to refresh system package manager metadata
before installing packages (apk: --update-cache, apt: apt-get update).
Hook phases can also run before and after the built-in steps:
pre-packages, post-packages, pre-repos, post-repos, pre-dotfiles,
post-dotfiles, pre-defaults, post-defaults, pre-user, post-user,
pre-tools, and post-tools.
The declarative steps converge: if a package is already installed, a repo is
already at the requested ref, a dotfile already matches, or a default is already
set, mise skips it. The bootstrap task runs every time, so keep it idempotent.
[bootstrap.packages]
"apk:build-base" = "latest"
"apt:build-essential" = "latest"
"brew:postgresql@17" = "latest"
[bootstrap.secrets]
service_token = "EXAMPLE_SERVICE_TOKEN"
[bootstrap.groups.example]
system = true
[bootstrap.users.example]
system = true
group = "example"
home = "/var/lib/example"
create_home = true
[bootstrap.directories."/opt/example"]
owner = "root"
group = "root"
mode = "0755"
[bootstrap.files."/etc/example.conf"]
content = 'token={{ secret(name="service_token") }}'
template = true
owner = "root"
group = "root"
mode = "0644"
notify = ["example"]
[bootstrap.services.example]
state = "running"
enabled = true
on_change = "reload_or_restart"
[bootstrap.linux.firewall]
backend = "auto"
state = "enabled"
default_incoming = "deny"
default_outgoing = "allow"
[[bootstrap.linux.firewall.rules]]
name = "https"
port = 443
protocol = "tcp"
action = "allow"
[bootstrap.repos]
"~/src/dotfiles" = { url = "[email protected]:jdx/dotfiles.git", ref = "main" }
[dotfiles]
"~/.gitconfig" = { mode = "symlink" }
"~/.config/nvim" = { mode = "symlink" }
[bootstrap.mise_shell_activate]
zprofile = "shims"
zshrc = "activate"
fish = "activate"
[bootstrap.macos.dock]
autohide = true
orientation = "left"
tilesize = 48
[bootstrap.macos.finder]
show_pathbar = true
[bootstrap.macos.keyboard]
key_repeat = 2
initial_key_repeat = 15
[bootstrap.macos.trackpad]
tap_to_click = true
[bootstrap.macos.defaults]
"com.apple.finder" = { AppleShowAllFiles = true }
[bootstrap.macos.launchd.agents.my-sync]
program = "~/.local/bin/my-sync"
args = ["--watch"]
run_at_load = true
[bootstrap.linux.systemd.units.my-sync]
description = "sync files"
exec_start = "~/.local/bin/my-sync --watch"
restart = "on-failure"
[bootstrap.user]
login_shell = "/bin/zsh"
[bootstrap.hooks.pre-packages]
run = "softwareupdate --install-rosetta --agree-to-license"
[bootstrap.hooks.post-defaults]
run = "killall Dock || true"
[tools]
node = "lts"
python = "3.12"
[tasks.bootstrap]
run = "gh auth status || gh auth login"
Then converge the whole machine (--yes skips the confirmation prompts):
mise bootstrap --yes
To preview what would change without touching anything:
mise bootstrap --dry-run
For a structured resource plan, use mise bootstrap plan. The provisioning
planner reports accounts, system packages, privileged files and directories,
system services, firewall policy and rules, and Compose projects in dependency order. Other declarative
bootstrap parts will join the same graph as they adopt the resource model.
mise bootstrap plan
mise bootstrap plan --json
mise bootstrap plan --detailed-exitcode
With --detailed-exitcode, the command exits 0 when nothing would change, 2
when the plan contains changes, and 1 when planning fails or any resource has
an unknown state. Unknown resources do not count as changes, but they block a
successful convergence result. A package is unknown when its manager is
unavailable on the current platform or cannot install the requested version.
This matches apply behavior: unsupported pins remain visible for manual
resolution instead of being reported as changes mise would skip.
When mise bootstrap applies or would apply something that needs user
follow-up, it prints a final bootstrap: follow-up section after a successful
run. Dry runs use bootstrap: follow-up if applied. If a later bootstrap phase
fails after earlier phases already produced follow-up items, mise prints those
items before returning the error. The section is omitted when there is nothing
actionable to report.
By default, bootstrap refuses dotfile conflicts rather than replacing local
files. Use mise bootstrap --force-dotfiles when you explicitly want the
dotfiles phase to replace conflicting whole-file dotfile targets.
Use mise bootstrap status to inspect the declarative bootstrap state in one
place. It reports every declarative part — packages, repos, dotfiles, shell
activation, macOS defaults, LaunchAgents, systemd units, and login shell —
plus [tools] and any system dependencies that installed tools require:
mise bootstrap status
mise bootstrap status --json
mise bootstrap status --missing
mise bootstrap packages status
mise bootstrap repos status
mise bootstrap dotfiles status
mise bootstrap dotfiles apply --dry-run
mise bootstrap dotfiles apply --dry-run --verbose
mise bootstrap mise-shell-activate status
mise bootstrap macos defaults status
mise bootstrap macos launchd-agents status
mise bootstrap linux systemd-units status
mise bootstrap firewall status
mise bootstrap user status
mise bootstrap status --missing checks the whole declarative bootstrap
surface in one command. The narrower mise bootstrap packages status --missing and mise bootstrap dotfiles status --missing commands are useful when you
only want to check one part without installing anything.
| Config | Use for |
|---|---|
[bootstrap.packages] | OS packages from apk, apt, dnf, pacman, brew, flatpak, or mas |
[bootstrap.repos] | Git repos cloned before dotfiles are applied |
[dotfiles] | Whole-file dotfiles and small managed edits to existing files |
[bootstrap.mise_shell_activate] | mise activation snippets in shell startup files |
[bootstrap.macos.*] | Curated macOS preferences for Dock/Finder/keyboard/trackpad |
[bootstrap.macos.defaults] | macOS user preferences written through defaults write |
[bootstrap.macos.launchd.agents] | macOS user LaunchAgents written and loaded with launchctl |
[bootstrap.linux.systemd.units] | Linux systemd user services managed with systemctl --user |
[bootstrap.linux.firewall] | Linux host firewall policy and managed rules |
[bootstrap.user] | Current-user settings such as login_shell |
[bootstrap.hooks] | Commands that run at named bootstrap phases |
[tools] | Versioned dev tools managed by mise |
[tasks.bootstrap] | Anything custom that should run after tools are installed |
Use declarative sections when mise can inspect and converge the state. Use
[tasks.bootstrap] for imperative setup that does not fit those sections,
such as running an auth flow, seeding local data, or other one-off project
setup.
Hooks run only during explicit mise bootstrap invocations. A hook can be
specified as a command string, an array of command strings, or a table with a
run field. They use the same default inline shell setting as tasks, stop the
bootstrap if they fail, and print the command instead of running it during
mise bootstrap --dry-run. Hooks run in the current process environment; use
mise exec -- ... inside a hook, or use [tasks.bootstrap], when the command
needs tools from [tools] on PATH.
[bootstrap.hooks.pre-packages]
run = "softwareupdate --install-rosetta --agree-to-license"
[bootstrap.hooks.post-tools]
run = [
"mise exec -- corepack enable",
"mise exec -- rustup component add rustfmt clippy",
]
[bootstrap.hooks.final]
run = "gh auth status || gh auth login"
As shorthand, a hook phase can also be set directly:
[bootstrap.hooks]
post-defaults = "killall Dock || true"
Hooks merge across the config hierarchy from global to local, so shared config
can define broad machine setup while a project adds its own phase commands.
The pre-dotfiles and post-dotfiles phases also wrap
mise bootstrap dotfiles apply.
mise trust
mise bootstrap --yes
mise bootstrap packages use apk:zlib-dev apt:libssl-dev
This writes [bootstrap.packages] and installs what is missing.
$EDITOR ~/.zshrc
mise bootstrap dotfiles add ~/.zshrc
mise bootstrap dotfiles add stores the live file under dotfiles.root and writes an
explicit [dotfiles] entry with mode.
mise bootstrap dotfiles edit ~/.zshrc
mise bootstrap dotfiles apply ~/.zshrc
For symlinked dotfiles, edit opens the managed source, so it works with the
default symlink mode.
You can manage the dotfiles repository and the mise global config as dotfiles:
[settings]
dotfiles.root = "~/.dotfiles"
[dotfiles]
"~/.dotfiles" = "~/src/dotfiles"
"~/.config/mise/config.toml" = "~/src/dotfiles/mise/config.toml"
The repo/source must exist before the first apply. Use the real repo path for
sources needed during the first run; ~/.dotfiles does not exist until mise
creates that symlink. Replacing the active global config affects future mise
invocations, so use this pattern carefully.