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 |
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 serverThe personal profile assumes macOS with Homebrew. On a fresh machine:
-
Install Homebrew (if not already installed):
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -
Clone and install dependencies:
git clone <repo-url> ~/dotfiles cd ~/dotfiles brew bundle # Installs everything from Brewfile
-
Install dotfile symlinks:
./install.sh personal
-
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) |
./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 |
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 editor-spawned shells like Windsurf's Cascade terminal). 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".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. Editors that spawn commands in a bare shell — notably Windsurf's Cascade terminal, but also cron and plain scripts — never run.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.