Personal dotfiles, managed with mox:
config files in their native format under src/, composed per machine from
axis overlays -- no filename prefixes, no template language in file bodies.
| Path | What lives there |
|---|---|
src/ |
Managed files, laid out exactly as they land under $HOME: src/.zshrc -> ~/.zshrc, src/.config/nvim/ -> ~/.config/nvim/ |
data/ |
Shared, committed data consumed while composing (data/abbreviations.toml, data/paths.toml, data/signing.toml, ...) |
data/packages/ |
Package manifests: one file per macOS/Linux backend, plus shared.toml for the blacklist that holds on every machine |
scripts/pre/, scripts/post/ |
Setup scripts run by mox apply, before/after files are written; each .sh carries a # mox: when os=... gate, the PowerShell ones live under os=windows/ |
scripts/check/ |
Check hooks a partially owned file names with # mox: check, run on its composed candidate before it is written |
etc/ |
Support library: the Windows package lists, CI helpers, tests, the agent-sandbox image, shared bash/PowerShell libraries |
Per-OS / per-profile variation uses mox's two idioms:
- Overlay filenames for structured files: a
.d/directory holds axis overlays, with or without a base file beside it, e.g.aerospace.toml.d/os=darwin.tomlcomposes only on macOS. - In-file directives for code and text: a leading
# mox: when os=darwingates a whole file; a# mox: when <expr> ... # mox: endregion gates a section.
Use one of the one-liners below to install mox (checksum-verified) and the dotfiles in a single step.
sh -c "$(curl -fsSL https://raw.githubusercontent.com/sakakibara/mox/main/install.sh)" -- init --clone sakakibara --applysh -c "$(wget -qO- https://raw.githubusercontent.com/sakakibara/mox/main/install.sh)" -- init --clone sakakibara --apply& ([scriptblock]::Create((irm https://raw.githubusercontent.com/sakakibara/mox/main/install.ps1))) init --clone sakakibara --applyTo review the repo before anything touches $HOME, drop --apply: the clone
lands in ~/.local/share/mox/dotfiles (the default MOX_REPO), and a later
mox apply composes and writes the live files. On first apply mox interviews
you for any missing machine-local facts (email, profile, locale, timezone,
signing keys) and stores them in $XDG_CONFIG_HOME/mox/facts.toml, which is
never committed to the repo.
Everything else is automated; these need GUI interaction or local credential bootstrap, once per new machine.
The public key and gitconfig are committed; each machine just needs the agent:
- Install 1Password (brew/scoop/distro package, or 1password.com).
- Sign in.
- Settings -> Developer -> "Use the SSH agent" -> on. If the wizard
offers to write an
IdentityAgentline into~/.ssh/config, decline -- the mox-managed~/.ssh/confighandles it, gated behind theuse_1password_ssh_agentfact. mox apply(if not already done).
Every commit is then signed (green "Verified" badge on GitHub;
git log --show-signature locally). Signing is gated on the
use_1password_ssh_agent fact together with the ssh side, so a machine that
answers no to it costs only the badge -- commits still work, unsigned.
The same key signs commits made inside agent-sandbox containers:
| Host | Setup | How the agent reaches the container |
|---|---|---|
| macOS | none | a socat relay on 127.0.0.1:19988, started on first launch (the macOS Unix socket can't be bind-mounted into Linux containers); socat comes from data/packages/darwin.toml |
| Linux | none | ~/.1password/agent.sock bind-mounted directly |
| Windows + WSL2 | 1Password app: Settings -> Developer -> Integrate with WSL -> on | ~/.1password/agent.sock appears inside WSL; run agent-sandbox from inside WSL (it's a bash script) |
| Native Windows | -- | out of scope; agent-sandbox doesn't run there |
The agent only exposes keys listed in the mox-managed agent config
(~/.config/1Password/ssh/agent.toml on macOS and Linux,
%LOCALAPPDATA%\1Password\config\ssh\agent.toml on Windows): the personal
signing key everywhere, a work key gated to profile=work, and any
identity in the local identities data that names a 1Password item. A
compromised container can request a signature with a listed key (each
request fires a host-side biometric prompt) but cannot enumerate or use any
other vault key.
- Generated the SSH signing key in 1Password.
- Pasted the public key into
data/signing.toml(personal_key). - Registered it on GitHub (Settings -> SSH and GPG keys, type "Signing Key").
Rotation: regenerate in 1Password, replace personal_key, register the new
public key on GitHub, optionally remove the old one.
Commit identity, signing key, and which account gh and HTTPS git
authenticate as are all chosen by the repo's remote URL -- never
per-machine, never a per-repo .git/config. The personal account is
committed; work/client accounts live only in machine-local mox config, so
employer names never reach this repo. Auto-switch resolves at invocation
time -- a gh shim in ~/.local/bin and the git credential helper both ask
account-token for the repo's token -- so it works with no gh auth switch
in shells, editors, scripts, and agent sessions alike. Windows mirrors the
design (gh.ps1/gh.cmd shims, the same credential helper, and a script
that keeps ~/.local/bin in front of the user Path on every apply, since
an installer can push it back).
Per-account git identity includes are generated from a machine-local,
private-layer identities data file that shadows the committed, empty one
(src/.config/git/id-leaves.gen is the generator, and mox removes the file
of a row that disappears, so removing an account also removes its include).
To add an account:
gh auth loginas the account (auto-switch reads its stored token).- Add it to the local identities data (email, match URLs, gh account).
- Optionally wire a signing key: its public key goes into
allowed_signersand, with a 1Password item name, into the agent config. mox apply.
mox apply runs scripts/pre/ before writing files and scripts/post/
after. mox runs a phase's top-level scripts in filename order, then its
os=... subdirectories, so every shell script sits at the top level with a
# mox: when os=... gate. pre/ holds only the Command Line Tools and, on
Windows, the scoop apps; everything else is post/, which runs after the
packages from data/packages/*.toml are installed and the files are written.
The one ordering among the post/ scripts is runtime-mise before tools,
which their names sort into. Every binary a script downloads is version-pinned
and checked against the checksum its project publishes, except where the
project publishes none: the Homebrew, holt and scoop installer scripts are
checked against a digest recorded in this repo, and theme assets against a sha256
recorded on first fetch. The cargo tools are the exception: cargo builds them
from crates.io at their current version, with --locked. A failed step fails
the script, which mox reports.
| Script | What it does |
|---|---|
pre/darwin-clt.sh |
the Xcode Command Line Tools, installed through softwareupdate when xcode-select -p finds none; sudo is asked for only then, and kept alive for the length of that install; packages are not a script's job -- they come from data/packages/*.toml (mox status reports drift, mox apply installs, mox commit records what you installed by hand) |
post/login-shell.sh |
on macOS, makes fish the login shell: adds it to /etc/shells with sudo tee -a and runs sudo chsh only when either is missing, so a converged machine is never asked for sudo; with no terminal and no cached sudo it fails naming the step |
post/runtime-mise.sh |
language toolchains via mise (installed from a pinned release when missing); runs post because the ruby and php builds need the packages' build dependencies, and mise install reads the applied ~/.config/mise/config.toml |
post/tools.sh |
Linux binaries outside the system package manager (lazygit, lazydocker, gh; starship where the distro does not package it; cargo tools through mise's rust, so it runs after runtime-mise) |
post/workspace-holt.sh |
installs holt if missing, links ~/Life/~/Work to the synced root, holt sync rebuilds project hubs; runs post so it can read the applied holt config |
post/theme.sh |
downloads theme assets per the manifests, verifies sha256, seeds the default; runs post because it needs ~/.local/bin/theme in place |
Windows runs the PowerShell counterparts under scripts/*/os=windows/:
apps-scoop.ps1 (scoop + winget via etc/windows/packages.txt),
runtime-mise.ps1, workspace-holt.ps1, theme.ps1, plus tools-path.ps1
and hide-dotfiles.ps1.
Outside the scripts, the tmux plugins are pinned by tag in ~/.tmux.conf;
tpm keeps a checkout it already has, so after a pin changes remove
~/.tmux/plugins and open tmux again.
dotfiles sits in front of mox: a status snapshot plus the steps mox
doesn't cover. Anything it doesn't recognize is forwarded to mox (with a
typo-aware error when the subcommand isn't valid for mox either).
| Command | What it does |
|---|---|
dotfiles / dotfiles info |
status snapshot: repo, branch, drift, theme, tools |
dotfiles apply / status / diff |
mox pass-through (apply re-sources the shell rc on success) |
dotfiles install |
interactive step menu, pre-checking steps whose inputs changed; install all runs every step, install none only the required ones, install mise holt only the named ones |
dotfiles cd |
change the calling shell's directory to the mox repo (a shell function in zsh and fish, and in PowerShell once the profile dot-sources ~/.config/powershell/dotfiles-shell.ps1) |
dotfiles edit <pattern> |
fuzzy-find a managed file, open its source via mox edit |
dotfiles profile [name] |
print the active profile / switch the profile fact and re-apply |
dotfiles doctor |
health-check mox, packages, theme, mise, holt |
dotfiles upgrade [--all] |
mox self-update; --all then brew (macOS), scoop and winget (Windows), mise + holt everywhere (Linux distro packages stay manual) |
dotfiles sync |
Windows only: review scoop/winget packages against etc/windows/packages.txt; macOS and Linux use mox commit instead |
Per-step install output lands in ~/.local/state/dotfiles/pick/logs/; a TSV
run history at ~/.local/state/dotfiles/pick/run-log.tsv.
Editing, the two mox motions: tweak a config by editing the live file
where it lives, then mox commit routes it back into src/ (an app's or
the OS's writes surface the same way -- mox apply reports the drift and
leaves the file untouched, so you choose: mox commit to keep the live
version, mox apply --overwrite to discard it). For structure -- overlays,
# mox: when regions, new variation -- edit the source: dotfiles edit <pattern> fuzzy-finds it, and the nvim integration applies on save, no
drift round-trip.
theme # print the current family/variant (also: theme get)
theme catppuccin/frappe # switch: explicit family/variant
theme catppuccin # switch: bare family name (default variant, if any)
theme set frappe # switch within current family
theme reload # re-fire reload signals without state change
theme resolve nvim [family[/variant]] # print a tool's name for the current (or given) theme
theme list [family] # discover families / variants
theme install [family[/variant]] # download missing assets, verified against the lockfile
theme refresh [family[/variant]] # force re-download, update lockfile
theme verify [family[/variant]] # check cache against lockfileOne switch restyles kitty, wezterm, tmux, nvim, herdr, fish colors, fzf and
vivid at once; ghostty and bat keep their own fixed theme. To add a family,
drop a manifest at
~/.config/dotfiles/themes/<family> (flat key=value; see
src/.config/dotfiles/themes/catppuccin for the shape) and run
theme refresh <family> to populate the lockfile; the fzf palette is
defined per family in the shell configs, so a new family needs a case there.