My personal dotfiles, managed with GNU Stow.
Two profiles are available, each built on a shared common package:
- server (
common+server) - Minimal bash+vim setup for remote machines. No zsh, no brew dependencies beyond stow itself. - personal (
common+personal) - Full desktop setup with zsh, neovim, wezterm, tmux plugins, oh-my-posh, and more.
| Package | Files |
|---|---|
common |
.aliases, .gitconfig, .dir-colors, .vimrc, .vim/ |
server |
.bashrc, .bash_profile, .tmux.conf |
personal |
.bashrc, .bash_profile, .zshrc, .zprofile, .zsh_plugins.txt, .tmux.conf, .tmux/, .config/nvim/, .wezterm.lua, .local/bin/, .ssh/config.d/, .pi/agent/models.json |
Some config is intentionally left out of this repo because this repo is public:
| Path | Why |
|---|---|
~/.ssh/config |
Contains internal hostnames, bastions, jump hosts, and usernames. Only the public-safe Host * defaults are tracked, as personal/.ssh/config.d/10-defaults.conf, pulled in by an Include near the bottom of the real file. That placement is load-bearing — see the comments in both files. |
~/.pi/agent/auth.json and siblings |
Provider credentials and machine state. Only models.json is tracked. |
The server profile only requires GNU Stow (and only at install time to create symlinks):
# Debian/Ubuntu
sudo apt install stow
# Fedora
sudo dnf install stow
git clone <repo-url> ~/dotfiles
cd ~/dotfiles
./install.sh serverFresh Macbook? Use the one-shot bootstrap. A brand-new Mac has no git, so
don't clone first — curl the bootstrap (curl is part of base macOS). It is
dotfiles-first: it installs Xcode Command Line Tools, Homebrew, and a
brew-managed git + stow, clones this repo with that git, then puts your
symlinks in place and sets zsh as the default shell — so you have a working
shell immediately. Only after that does it install the full Brewfile package
set, as a best-effort step: a failed package download (a flaky cask CDN, say)
just gets reported and never blocks your shell or dotfiles. Idempotent and safe
to re-run:
curl -fsSL https://raw.githubusercontent.com/kmuncie/dotfiles/master/bootstrap.sh -o /tmp/bootstrap.sh
bash /tmp/bootstrap.sh personalAlready have the repo cloned? Just run ./bootstrap.sh personal from inside it.
On git and the Command Line Tools: Homebrew requires the Xcode CLT, so they're always installed — but the bootstrap never depends on Apple's git for anything you care about. It
brew installs git up front and clones with that, and the dotfiles PATH puts/opt/homebrew/binahead of/usr/bin, so everygityou run is the brew-managed one. CLT git only serves Homebrew's internals.
Then apply system preferences and finish the account/GUI steps:
./macos-defaults.sh # keyboard, Finder, Dock, screenshots, window behaviorSee docs/new-device.md for the full new-machine
checklist (GPG/YubiKey, app sign-ins, GUI-only settings).
Manual steps (what bootstrap.sh automates)
-
Install Homebrew (if not already installed):
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -
Install the seed tools, then clone:
brew install git stow git clone <repo-url> ~/dotfiles cd ~/dotfiles
-
Install dotfile symlinks (do this before the big package install):
./install.sh personal
-
Install the full package set (best-effort — re-run if a download fails):
brew bundle # everything from Brewfile -
Set zsh as default shell (if not already):
chsh -s $(which zsh) -
Initialize tmux plugins (first time): Open tmux, then press
prefix + Ito install plugins via TPM.
These are installed via brew bundle from the Brewfile:
| Dependency | Used By |
|---|---|
stow |
Symlink management |
antidote |
Zsh plugin manager (loaded in .zshrc) |
oh-my-posh |
Zsh prompt theme |
neovim |
Editor (LazyVim config in .config/nvim/) |
wezterm |
Terminal emulator |
tmux |
Terminal multiplexer |
git-delta |
Git pager (configured in .gitconfig) |
fzf |
Fuzzy finder |
coreutils |
GNU ls, dircolors (used by .dir-colors) |
gnu-sed |
GNU sed |
grep |
GNU grep |
gnupg + pinentry-mac |
GPG commit signing (YubiKey) |
granted |
AWS role assumption (assume function) |
llama.cpp |
Local GGUF inference, driven by llamactl (see docs/local-llm.md) |
./cleanup-legacy.sh # Remove old symlinks safely
./install.sh personal # Install new stow-based symlinksThe install script is idempotent — safe to re-run at any time:
./install.sh personalWhen you need to re-run: only when a new file is added to a stow package
(e.g. adding personal/.sometool). The new file needs a new symlink.
When you don't need to re-run: editing existing files. Since stow creates symlinks, changes to files in the repo are picked up immediately.
./uninstall.sh personal # Remove personal profile symlinks
./uninstall.sh server # Remove server profile symlinks
./uninstall.sh all # Remove all stow-managed symlinksThese files live at the repo root and are referenced by path rather than symlinked:
| File | Purpose |
|---|---|
oh-my-posh/ |
Prompt themes (referenced by .zshrc) |
scripts/ |
Utility scripts, plus llamactl-wired-limit.sudoers (installed by hand into /etc/sudoers.d/) |
Brewfile |
Homebrew dependencies (brew bundle) |
themes.gitconfig |
Git delta themes (included by .gitconfig) |
neofetch.conf |
Neofetch config (referenced by alias) |
.git-autocomplete.sh |
Bash git completion (sourced by .bash_profile) |
ascii/ |
ASCII art |
iterm/ |
Legacy iTerm color schemes |
terminatorThemes/ |
Legacy Terminator themes |
Setup using the following guide: YubiKey-Guide
The personal profile uses a layered approach to shell configuration:
-
~/.profile: Shared environment sourced by.zshenv,.zprofile, and.bash_profile. Contains PATH, Homebrew init, environment variables (EDITOR,GOPATH,GPG_TTY). This is the single source of truth for environment setup — every shell gets identical PATH and variables. Safe to source more than once: the PATH block hard-resetsPATHbefore rebuilding it, so the result is deterministic rather than cumulative. -
~/.zshenv: Runs for every zsh — login, interactive, and non-interactive (scripts, cron, and the bare shells that editors and AI coding tools spawn). Sources.profileso PATH exists even when no login or interactive file runs. See "Why PATH is sourced in more than one place" below. -
~/.zprofile: Zsh login wrapper. Sources.profile. -
~/.bash_profile: Bash login wrapper. Sources.profile, then adds bash-specific bits (bash-completion, dircolors, git-autocomplete). -
~/.zshrc: Runs for every interactive zsh. Aliases, functions, keybindings, completion, plugins, prompt. -
~/.bashrc: Runs for every interactive bash. Aliases, PS1, history.
All PATH modifications should be made in personal/.profile. This keeps
PATH consistent across both zsh and bash, and prevents it from growing
incorrectly with every new terminal window.
# From `brew install my-new-tool`
# This tool needs its bin directory in the PATH.
[ -d "/path/to/my-new-tool/bin" ] && export PATH="/path/to/my-new-tool/bin:$PATH"nvm is initialized in .zshrc, which runs for interactive shells only.
Scripts get .zshenv → .profile, which hard-resets PATH and rebuilds it
without nvm's node bin. The parent shell's PATH does not survive into a child
script, so anything installed with npm install -g is not found there — even
though it works fine when typed in a terminal.
This bites twice: first as command not found: <tool>, then, once the binary is
located, as env: node: No such file or directory, because npm shims start with
#!/usr/bin/env node and need node on PATH too.
llamactl works around it locally by resolving the binary itself and prepending
its directory (see find_pi in personal/.local/bin/llamactl). That fix is
per-script and does not help anything else.
Possible future change, deliberately not made yet: add nvm's default-alias
bin directory to .profile, which would fix this for every script at once:
# Reads the version nvm's `default` alias points at, e.g. "24.18.0", and puts
# that bin on PATH for non-interactive shells too. Verified to resolve both
# `node` and `pi` in a clean non-interactive zsh; not adopted into .profile.
NVM_DEFAULT="$(cat "$HOME/.nvm/alias/default" 2>/dev/null)"
[ -n "$NVM_DEFAULT" ] && [ -d "$HOME/.nvm/versions/node/v$NVM_DEFAULT/bin" ] \
&& export PATH="$HOME/.nvm/versions/node/v$NVM_DEFAULT/bin:$PATH"Weigh before adopting: it pins scripts to the default Node version rather than
whichever version nvm use selected, so a script and an interactive shell could
run different Node versions and disagree. It also makes .profile depend on
nvm's internal alias/default file layout. That is why this is a note rather
than a change — it alters shared PATH behavior for every shell and script.
.profile is sourced from both .zshenv and .zprofile. This looks like
duplication but each call solves a distinct macOS quirk — removing either one
reintroduces a real bug:
-
.zshenv→ guarantees PATH exists at all. zsh only reads.zshenvfor non-login, non-interactive shells. Anything that spawns commands in a bare shell — AI coding tools and editor terminals, but also cron and plain scripts — never runs.zprofileor.zshrc. Without sourcing.profilehere,/usr/local/binis missing and tools installed there (e.g. the 1PasswordopCLI) silently fail to resolve. -
.zprofile→ re-asserts PATH ordering afterpath_helper. macOS's/etc/zprofilerunspath_helper, which rebuilds PATH from/etc/pathsand runs after.zshenv. It shoves the system/usr/binahead of our Homebrew GNU tools (coreutils,gnu-sed,grep), sosed/grep/lswould resolve to the BSD versions. Re-sourcing.profilefrom.zprofile(which runs afterpath_helper) restores the intended ordering.
The only single-source alternative is disabling path_helper by editing the
system file /etc/zprofile — more invasive and lost on OS updates, so we keep
the two-source approach instead.
Note:
~/.zshenvis currently a plain file in$HOME, not yet a stowed file inpersonal/. To make the.zshenv → .profilesource survive a fresh install, move it into thepersonalpackage and re-run./install.sh personal.
All custom keybindings use the leader key (Ctrl+Space) followed by another key:
- Leader + \: Split horizontally (new pane to the right)
- Leader + -: Split vertically (new pane below)
- Leader + h/j/k/l: Navigate between panes (vim-style)
- Leader + r: Enter resize mode (then use h/j/k/l to resize)
- Leader + c: Close current pane (with confirmation)
- Leader + p: Open project picker (fuzzy search through ~/code directory)
- Leader + f: Show workspace fuzzy finder
The configuration automatically discovers projects in the ~/code directory and
allows quick switching between them. Each project opens in its own workspace.