Onboarding Runbook¶
Step-by-step setup for a new machine. The core workflow is identical on macOS and Linux. Platform-specific steps (prerequisites, SSH agent, optional GUI tools) are presented in tabbed sections.
Prerequisites¶
- macOS with Command Line Tools (
xcode-select --install) - A GitHub account with SSH key access
-
1Password (optional). If you want it for GitHub passkey authentication or SSH commit signing, install it before bootstrapping:
-
Install core dependencies:
-
Set zsh as the default shell:
-
A GitHub account with SSH key access
-
1Password (optional). If you want it for GitHub passkey authentication or SSH commit signing, add the 1Password apt repository before bootstrapping, then install the desktop app and the
opCLI from it:curl -sS https://downloads.1password.com/linux/keys/1password.asc \ | sudo gpg --dearmor \ --output /usr/share/keyrings/1password-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/1password-archive-keyring.gpg] https://downloads.1password.com/linux/debian/$(dpkg --print-architecture) stable main" \ | sudo tee /etc/apt/sources.list.d/1password.list sudo apt update && sudo apt install 1password 1password-cli
- Complete the Fedora setup page
first — it covers system update, dnf configuration, developer
packages (including
zsh,git, andutil-linux-user), and setting zsh as the default shell. - A GitHub account with SSH key access
-
1Password (optional). If you want it for GitHub passkey authentication or SSH commit signing, add the 1Password dnf repository before bootstrapping, then install the desktop app and the
opCLI from it:sudo rpm --import https://downloads.1password.com/linux/keys/1password.asc sudo sh -c 'echo -e "[1password]\nname=1Password Stable Channel\nbaseurl=https://downloads.1password.com/linux/rpm/stable/\$basearch\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=\"https://downloads.1password.com/linux/keys/1password.asc\"" > /etc/yum.repos.d/1password.repo' sudo dnf install 1password 1password-cli
Step-by-step setup¶
Step 1: Clone the dotfiles¶
The repository lives inside the profile tree, not at ~/dotfiles: it
is a personal repo, so it belongs under ~/development/personal/repos/
where the path-based git identity and the personal mise profile apply
to it like any other repo. Export the path once; the rest of this
runbook uses $DOTFILES:
Clone over HTTPS, not SSH. This is deliberate: the repository is
public, so an HTTPS clone needs no credentials — and at this point you
have none. Your SSH keys do not exist yet (Step 5), and the GitHub
passkey in 1Password is not wired up for git either. Cloning over SSH
here would fail with Permission denied (publickey). You will switch
this clone's remote to SSH in Step 7, once the keys exist.
(git clone creates the missing ~/development/personal/repos/
parents itself; bootstrap.sh creates the sibling work/ and
opensource/ trees in the next step.)
Step 2: Bootstrap the filesystem¶
This prepares the filesystem only — it installs no software:
- creates XDG directories and the
~/development/{personal,work,opensource}/reposprofile tree - symlinks configuration files, including the mise configs:
config.toml, the early-initmiserc.toml(which carriesauto_env), and the per-OSconfig.linux.toml/config.macos.toml— files mise's own[dotfiles]pass cannot self-apply because they decide what mise loads - copies the editable seeds: git identity templates, the per-profile
mise.tomlfiles under~/development/, and~/.ssh/config - audits shell startup files for rogue installer injections
Review the output — anything marked [rogue] needs cleanup.
Step 3: Verify zsh is the login shell¶
The platform setup page covers changing the default shell. Verify it took effect before continuing:
echo $SHELL
# Should print the path zsh was installed at: /usr/bin/zsh or
# /bin/zsh on Linux (distro-dependent), /bin/zsh on macOS.
# If it still shows bash, log out and back in first.
If you are in a bash session and need to continue immediately, start zsh manually:
Then confirm no leftover bash startup files remain. If
~/.bash_profile or ~/.bash_login exists, bash reads it instead
of the framework's ~/.profile, and the POSIX shim that supplies
environment variables to cron, systemd user services, and other
non-zsh subprocesses never loads:
ls -la ~/.bash_profile ~/.bash_login ~/.bashrc 2>/dev/null
# Expected: no such files. If any exist, remove them:
rm -f ~/.bash_profile ~/.bash_login ~/.bashrc
The platform setup pages cover this in their shell-change step;
this check catches the case where a file was re-created (for
example by a tool installer) after that step. See when
~/.profile is
read.
Run the remaining steps in a framework shell
Every step from here on installs or first-runs a tool, and several
of those tools place their state based on an environment variable
the framework sets — CARGO_HOME, RUSTUP_HOME, GNUPGHOME,
GOPATH. If you run them from the original bash login session, those
variables are not set, and the tools fall back to ~/.cargo,
~/.rustup, ~/.gnupg, and ~/go in your home directory instead of
their XDG locations.
Confirm you are in a shell that has loaded the framework environment before continuing:
echo "$CARGO_HOME"
# Should print: <your XDG_DATA_HOME>/cargo (e.g. ~/.local/share/cargo)
# If it prints nothing, start a framework shell first: exec zsh
This will not stop the genuinely unrelocatable directories
(~/.mozilla, ~/.pki) from appearing — those honor no such
variable and no framework lever moves them. ~/.vim is not in that
category: it is a deliberate config placement, not a leak. See
directories that escape
XDG
for which leaks are preventable, which are the cost of the tool, and
why Vim is neither.
Step 4: Provision everything with mise¶
There is no curl | sh step. bin/mise is vendored in the repository
and version-pinned: on first run it downloads that exact mise release,
verifies its checksum, caches it, and executes it.
One command replaces what used to be four separate installs (mise
itself, the usage CLI, rv, and mise install):
- System packages from
[bootstrap.packages]in the per-OS config (zsh, libsecret, ffmpeg, ImageMagick, the mysql client). This is the part that escalates: mise shells out tosudo dnf|apt|brew installas its first step, before anything else — run it deliberately. - The symlink farm from
[dotfiles], converging whatbootstrap.shalready applied.bootstrap.shstays authoritative for the early-init files mise cannot self-apply (see Step 2). - Every tool in
[tools]—rvincluded (resolved from GitHub releases via thegithub:backend, so itscurl | shinstaller is gone too), with the modern installers doing the work:uv tool installforpipx:tools,bunfornpm:tools,cargo-binstallforcargo:tools.usageis in[tools]as well, so mise's shell completions work from the first reload.
Afterwards, reload the shell so the new tools are on $PATH and
verify:
Never mise use -g
mise use -g <tool> rewrites the global config file to add a pin —
but that file is a symlink back into your dotfiles repo, so the
rewrite dirties your working tree (or clobbers the symlink).
Declarations live in version control: add the tool to
mise/config.toml, then run mise install to materialize it.
Step 5: Generate SSH keys¶
# Work key
ssh-keygen -t ed25519 \
-C "dev@zftadvancements.com (work, $(hostname), $(date +%Y-%m))" \
-f ~/.ssh/id_ed25519_work
# Personal key
ssh-keygen -t ed25519 \
-C "you@billwoika.com (personal, $(hostname), $(date +%Y-%m))" \
-f ~/.ssh/id_ed25519_personal
Step 6: Edit identity templates¶
Run these from a framework shell so $EDITOR is set (it is exported by
the zsh startup chain — if $EDITOR expands to nothing, you are not in a
framework shell yet; run exec zsh first, or substitute vim):
$EDITOR ~/.config/git/local.config # user.name (shared across profiles)
$EDITOR ~/.config/git/work.config
$EDITOR ~/.config/git/personal.config
$EDITOR ~/.config/git/opensource.config
$EDITOR ~/.config/git/allowed_signers
$EDITOR ~/.ssh/config
local.config holds user.name only — bootstrap copies it from
git/local.config.example (placeholder Your Name). Email, signing
keys, and host aliases stay in the path-based profile files.
opensource.config applies to repos cloned under
~/development/opensource/; if
you do not use that directory you can leave it, but note it ships with
placeholder identity, so a repo cloned there would otherwise commit
under the template email.
Step 7: Register SSH keys¶
Add both keys to GitHub (Settings > SSH and GPG keys): - Once as "Authentication Key" - Once as "Signing Key"
Load keys into the agent:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519_work
ssh-add --apple-use-keychain ~/.ssh/id_ed25519_personal
The --apple-use-keychain flag stores the passphrase in macOS
Keychain so the key is available across reboots without re-entry.
On GNOME 46+ (Fedora 40+, Ubuntu 24.04+), the SSH agent is no
longer part of gnome-keyring. GNOME 46 deprecated that component
and moved it to gcr-ssh-agent (the gcr-4 package), whose
systemd user socket lives at $XDG_RUNTIME_DIR/gcr/ssh. On a
default Fedora Workstation this is already enabled — verify:
echo $SSH_AUTH_SOCK
# GNOME 46+ : /run/user/<uid>/gcr/ssh
# (older : /run/user/<uid>/keyring/ssh — pre-GNOME-46)
If $SSH_AUTH_SOCK is empty, the gcr agent socket is not active.
Enable it and re-login (or set the variable for the current shell):
systemctl --user enable --now gcr-ssh-agent.socket
# current shell, until next login:
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/gcr/ssh"
Then add your keys:
With AddKeysToAgent yes in ~/.ssh/config, keys are added
automatically on first use for the duration of the session.
Without a desktop agent, create a systemd user service:
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/ssh-agent.service <<'EOF'
[Unit]
Description=SSH key agent
[Service]
Type=simple
ExecStart=/usr/bin/ssh-agent -D -a %t/ssh-agent.socket
[Install]
WantedBy=default.target
EOF
systemctl --user enable --now ssh-agent
The framework's ~/.profile detects the agent socket
automatically. Verify after restarting the shell:
Then add your keys:
With AddKeysToAgent yes in ~/.ssh/config, keys are added
automatically on first use for the duration of the session.
Switch the dotfiles remote to SSH¶
Now that your keys exist and are registered, point the dotfiles clone
(cloned over HTTPS in Step 1) at your SSH host alias so future pulls and
pushes use the key. Use the host alias you defined in ~/.ssh/config
(Step 6) — github.com-personal here, the repo living under
~/development/personal/ like any other personal repo:
cd "$DOTFILES"
git remote set-url origin git@github.com-personal:billwoika/dotfiles
git remote -v # confirm origin now shows the SSH URL
Step 8: Validate¶
# Full framework health check: required capabilities, the
# rogue-injection audit, the POSIX profile test suite, then
# `mise bootstrap status`
mise run dotfiles:doctor
# Verify mise itself
mise doctor
Verify SSH authentication to GitHub. This requires that you finished
Step 7 — both the host aliases in your edited ~/.ssh/config and the
keys registered at GitHub. The success message is GitHub greeting you by
username (it then closes the connection — GitHub does not allow shell
access, so "does not provide shell access" is the expected, healthy
result):
ssh -T git@github.com-work
ssh -T git@github.com-personal
# Expected: "Hi <your-username>! You've successfully authenticated, but
# GitHub does not provide shell access."
# "Permission denied (publickey)" means the key is not registered, or
# the host alias is missing from ~/.ssh/config.
Verify git identity. The framework selects your work vs. personal
identity by directory, using includeIf "gitdir:..." rules — and those
rules only fire inside a git repository under ~/development/work/
or ~/development/personal/. The dotfiles clone itself is one such
repository — git config user.email inside $DOTFILES should already
show your personal email. Verify the work profile after your first
work clone:
cd ~/development/work/repos
git clone git@github.com-work:your-org/some-repo # any work repo
cd some-repo
git config user.email # NOW shows your work email
Step 9: Re-check for regenerated bash startup files¶
The Fedora setup page had you delete ~/.bashrc, ~/.bash_profile, and
~/.bash_login before bootstrapping. But several tools installed during
this runbook can append to or re-create ~/.bashrc (or
~/.bash_profile) as part of their first run,
after you deleted it. A regenerated ~/.bash_profile silently shadows
the framework's ~/.profile again: bash reads the bash file and never
falls through to ~/.profile, so the POSIX subprocess shim stops loading
for cron and systemd user services.
Run this last, once every tool above is installed, to catch anything that came back:
# List any bash startup files that reappeared
ls -la ~/.bashrc ~/.bash_profile ~/.bash_login 2>/dev/null
# Remove any that did. (You are not a bash user under this framework;
# these are installer cruft that re-shadows ~/.profile.)
rm -f ~/.bashrc ~/.bash_profile ~/.bash_login
Then re-run the bootstrap audit, which scans the remaining startup files
for rogue installer-injected PATH lines (it reports them but does not
delete — that part is on you):
If an installer re-injected a PATH export into ~/.profile or a zsh
startup file, the audit flags it [rogue]. Remove those lines and rely
on conf.d/10-path.zsh instead.
Optional steps¶
GUI editors and their CLI wrappers¶
TextMate, MarkEdit, and duti are declarative — brew-cask: /
brew: entries in mise/config.macos.toml — so Step 4 already
installed them. Their mate / markedit CLI shortcuts are
detected from /Applications, which mise's [dotfiles] cannot
express, so re-run bootstrap once to create them:
iTerm2 stays a manual install:
Configure file associations¶
Add mise shims to system PATH (for GUI IDEs)¶
System packages¶
Nothing left to install by hand: bat, fd, ripgrep, fzf,
and direnv are declared in mise's [tools] (aqua-backed), and
libsecret-tools (providing secret-tool for the keychain_get
shell function) is an apt: entry in mise/config.linux.toml —
Step 4 delivered all of them.
SSH agent persistence¶
See the systemd user service in Step 7 above. On GNOME 46+
desktops (Ubuntu 24.04+), the SSH agent is provided by
gcr-ssh-agent, not gnome-keyring — its socket is
$XDG_RUNTIME_DIR/gcr/ssh, normally already active.
Add mise shims to system PATH (for GUI IDEs)¶
System packages are covered in the
Fedora setup page;
if that page was followed, they are already installed. bat,
fd, ripgrep, fzf, and direnv are not among them — they are
mise-managed (aqua-backed) and arrived with Step 4.
SSH agent persistence¶
See the systemd user service in Step 7 above. On Fedora
Workstation (GNOME 46+), the SSH agent is provided by
gcr-ssh-agent, not gnome-keyring — its socket is
$XDG_RUNTIME_DIR/gcr/ssh, enabled by default.
Add mise shims to system PATH (for GUI IDEs)¶
Cloning a project¶
After the machine is set up, the per-project workflow:
cd ~/development/work/repos
git clone <repo-url>
cd <project>
# One-time project setup (if mise.toml defines a setup task)
mise trust
mise run setup
# Or manually
mise install # install project-pinned tools
rv clean-install # Ruby dependencies (if Ruby project)
uv sync # Python dependencies (if Python project)
bun install # JS dependencies (if JS project)
docker compose up -d # local services
On Fedora the docker command is supplied by podman-docker (an
alias to Podman), but that package does not provide compose.
For docker compose / podman compose to work you also need a
compose provider — install podman-compose (it is already in the
Fedora developer-prerequisites list):