The whole machine is described declaratively in a single file — mise.toml.
macOS only.
It is applied by mise bootstrap: system
packages, git repos, dotfile symlinks, login shell and tools.
Five steps. You will be asked for a password twice — that is normal, it is spelled out below where.
On a clean macOS git and make are stubs, so the very first command pops up
the GUI installer. Better to launch it up front and wait:
xcode-select --install[tools] is almost entirely on latest, so mise goes to api.github.com for
versions, where anonymous access gets 60 requests per hour — not enough for a
toolset this size. Normally the token comes from gh
(github.credential_command in [settings]), but on a fresh machine gh is not
authenticated yet, so for the first run the PAT has to be pulled out of 1Password
by hand (from a phone or the web). Without a token bootstrap does not fail, but it drowns in 429s and
retries.
There are no ssh keys on a new machine yet, so not git@:
git clone https://github.com/mokevnin/dotfiles.git ~/dotfiles
cd ~/dotfilesLater, once the keys are in place: git remote set-url origin git@github.com:mokevnin/dotfiles.git.
export GITHUB_TOKEN=… # from step 2
make installmake install = install Homebrew, install mise with it, and run mise bootstrap --yes. Nothing has to be on the machine beforehand. Run it from
~/dotfiles — why, is explained below in "What lives where". The --yes flag
is also what keeps mise from asking whether you trust the config.
What happens, in order:
brew target |
installs Homebrew if it is missing — asks for a password |
mise target |
brew install mise |
pre-packages hook |
the same Homebrew installer, for the case where bootstrap is run without the Makefile. Here brew already exists, so it does nothing |
[bootstrap.packages] |
brew formulae and casks, GUI apps included. pkg casks (docker-desktop, zoom, nordvpn) go through installer(8) and ask for a password |
[bootstrap.directories] |
creates ~/Downloads/Screenshots, the folder the screenshot preference below points at. macOS does not create it, and a screenshot taken into a missing folder goes nowhere |
[bootstrap.repos] |
clones oh-my-zsh — only its plugin files are used |
[dotfiles] |
symlinks ~/.config/nvim, ~/.config/mise/config.toml, ~/.gitconfig, ~/.config/starship.toml, ~/.config/zsh/rc.zsh, and writes the one line of .zshrc that sources the last of them |
[bootstrap.user] |
login shell set to /bin/zsh. macOS has shipped zsh as the default since Catalina, so on a stock mac this is already true and nothing happens — the line is here to state it, not to change it |
[bootstrap.macos] |
the preferences that were changed on this machine by hand: Dock on the left and hidden, tap to click, every text substitution off, screenshots into ~/Downloads/Screenshots. Only these — anything still at the macOS default is deliberately not declared. Neither Dock nor Finder is restarted, so killall Dock afterwards |
mise install |
the whole toolset, yc included. The longest part |
If mise is already on the machine, steps 3–4 collapse into a single command:
mise bootstrap --from https://github.com/mokevnin/dotfiles.git --from-dir ~/dotfiles --yesexec zshUntil then none of this exists in the current session — .zshrc was written
after it had already started.
The repo describes the machine, but not the accounts. These will not show up on their own:
- ssh keys and
~/.ssh/config— the keys live in the 1Password agent (IdentityAgent), and the same agent signs commits (op-ssh-signin.gitconfig) - logins:
gh auth login,glab auth login,atuin login, claude, codex - tokens:
~/.npmrc(npmjs + npm.pkg.github.com), plusCODEX_GITHUB_PERSONAL_ACCESS_TOKENandYANDEX_TRACKER_MCP_TOKENin~/.config/zsh/local.zsh— the one shell file that is deliberately untracked, sourced last byrc.zsh. This repo is public, so nothing secret can live in it;local.zshis where those exports go instead ~/.kube,~/.dockerand the rest of the client state- the first launch of Docker Desktop and 1Password — they will ask for system permissions
To check that everything lined up: mise bootstrap status — every line there
should read installed/applied. That covers the declarative parts only; the
hand-made half of this list is checked by mise doctor project, which probes
the logins, the 1Password ssh agent and the npm tokens and prints a hint for
whatever is still missing.
make install # the only thing make is for: brew, then mise, then bootstrap
mise bootstrap --yes # apply the config
mise bootstrap --dry-run # see what would change
mise bootstrap status # state of every declarative part
mise bootstrap --only tools # apply just one part
mise upgrade # update the [tools] half only
mise run upgrade # update everything: the tools and the brew side
mise run lint # actionlint, stylua, taplo, typos, gitleaks
mise doctor project # check the logins, keys and tokens done by handThe Makefile exists for exactly two things: installing Homebrew and installing mise with it — the only pieces that cannot install themselves. Everything else is a mise command.
mise upgrade is only half an update: it walks [tools] and stops there, and
[bootstrap.packages] describes the machine at bootstrap time rather than
tracking it afterwards. mise run upgrade is the one that updates both halves.
It calls Homebrew and mise bootstrap packages upgrade, because a cask is
owned by whichever of the two installed it and neither reads the other's
bookkeeping: mise leaves a .mise-cask.toml in the Caskroom where brew expects
a .metadata directory. So brew skips the casks mise installed, and mise
declines the ones brew did. The visible symptom is that brew doctor reports
"invalid metadata" for mise's casks while mise bootstrap packages status calls
brew's "missing" — chasing one clean breaks the other, and the applications
themselves are fine either way.
CI (.github/workflows/main.yml) runs
mise bootstrap --dry-run on macos-latest and then the very same
mise run lint, so the lint is described in one place and not duplicated in the
workflow.
~/.zshrc is generated and holds exactly one line — it sources
zsh/rc.zsh, where the whole config lives, ordering included.
mise creates .zshrc itself when it is missing, so nothing has to put one on a
fresh machine: zsh is already the macOS default.
There is no oh-my-zsh framework here, but its plugins are still used. git,
vi-mode and eza are ordinary zsh scripts and get sourced straight out of the
clone — the git aliases (gst, gd, gpra, ...) come from upstream and are not
retyped here. What is skipped is everything around them: themes, plugins=(),
oh-my-zsh.sh, the updater. That was most of the startup time and none of the
value; the prompt is starship instead, configured to look like
the robbyrussell theme it replaced.
The tradeoff is that plugins/<name>/<name>.plugin.zsh is an internal path
rather than a promised interface, and [bootstrap.repos] tracks master. If an
upstream change ever breaks a plugin, pin ref to a tag.
Completions are generated, not tracked. Almost none of the tools in [tools]
ship a completion file — each prints one from a subcommand, and the flag differs
per tool. mise-completions-sync
keeps the table of those flags for 130-odd tools, which is a table this repo then
does not have to keep; it is declared in [tools] like everything else and
called from two hooks in mise.toml. postinstall runs after every
mise install or mise upgrade and regenerates only what changed;
[bootstrap.hooks.post-tools] does the full pass on a fresh machine, at the
first moment the tool itself exists, and installs mise's own _mise alongside.
The four tools its registry does not know about — yc, docker-cli,
sentry-cli, taplo — are in zsh/completions-registry.toml,
which is merged on top of the built-in one. Note that the override is read
through the dirs crate, so on macOS it belongs in ~/Library/Application Support, not the ~/.local/share path the upstream README names.
rc.zsh itself generates nothing at startup. It puts the two finished
directories on fpath before compinit: the one misecompsync writes and
~/.local/share/zsh/site-functions, where mise completion zsh --install puts
_mise. They are kept apart so misecompsync clean cannot reach the latter.
mise.toml section |
What it describes |
|---|---|
[tools] |
Languages and CLI utilities. Backends: registry, npm:, gem:, pipx:, github:, http: |
[bootstrap.packages] |
System packages and GUI apps. brew:/brew-cask: are installed by mise through Homebrew itself |
[bootstrap.repos] |
The oh-my-zsh clone, for its plugin files |
[dotfiles] |
Symlinks (~/.config/nvim, ~/.config/mise/config.toml, ~/.gitconfig, ~/.config/starship.toml, ~/.config/zsh/rc.zsh) and the single source line in .zshrc |
[bootstrap.user] |
Login shell |
[bootstrap.hooks.pre-packages] |
Installs Homebrew on macOS before the brew: packages |
[doctor.checks.*] |
Probes for the accounts the repo cannot install — run by mise doctor project |
[tasks.upgrade] |
Updates both halves of the machine — mise upgrade for [tools], brew upgrade for everything Homebrew owns |
[tasks.lint] |
actionlint, stylua, taplo, typos and gitleaks — the same task locally and in CI |
mise.toml is symlinked into ~/.config/mise/config.toml, so the tools are
global and available from any directory. But mise bootstrap has to be run
from ~/dotfiles: the sources in [dotfiles] are relative, and mise
resolves them against whichever config file it loaded through — from the home
directory that is ~/.config/mise/, and every source "disappears". Absolute
paths do not help here: then CI breaks, where the repo does not live in
~/dotfiles.
To add a tool — a line in [tools], then mise install.
To add a system package — mise bootstrap packages use brew:foo.
To pull a changed dotfile into the repo — mise bootstrap dotfiles add ~/.foo.
To see what is actually installed right now — mise ls.
The repository is public, so dotfiles add is the one place a secret could leak
in here: the command copies the whole file. Look at what actually arrived before
committing. Tokens and keys are not kept here and must not be — they belong in
1Password. The only sensitive-looking thing in .gitconfig is user.signingkey,
and that is the public half of an ssh key, exactly the one already sitting at
https://github.com/mokevnin.keys.
LazyVim, config in nvim/.