Personal shell environment, evolved over 12+ years across Ubuntu, macOS, bash, and zsh.
git clone git@github.com:garrow/dotfiles.git ~/.dotfiles
cd ~/.dotfiles
./setup.sh # Shell configs & symlinks only
./setup.sh install # Also install apps via Homebrew / apt
./setup.sh menu # Interactive pickerSetup symlinks configs into $HOME — the repo is the live source of truth, edits take effect immediately.
If you're designing your own dotfiles, these are the structural ideas that have held up over time.
Everything is a small .sh file in a core/ or plugins/ directory. The bootstrap loader (bootstrap.sh) walks these directories and sources every *.sh file alphabetically:
shared/core/aliases.sh # Cross-shell basics
shared/core/functions.sh
shared/plugins/git.sh # One tool = one file
shared/plugins/spotify.sh
zsh/core/paths.sh # Shell-specific overrides
zsh/plugins/starship.sh
To add a capability, drop in a file. To remove one, delete it or rename to .disabled. Load order is controlled by filename prefixes (00_homebrew.sh before 01_shim_nvm.sh).
The shared/ directory holds everything that works in both bash and zsh — aliases, functions, and plugins. Shell-specific code lives in bash/ or zsh/. The loader in zsh/zshrc-actual sources shared first, then zsh-specific:
shared/core → zsh/core → shared/plugins → zsh/plugins
- This repo — the base, version-controlled config
~/.extras.rc.sh— device-local overrides (directory bookmarks, machine-specific paths). Not committed.~/work/dotfiles/init.rc.sh— employer-specific config in a separate repo, loaded last via git[include]
Tools like nvm, sdkman, conda, and pyenv are expensive to initialize. The 01_shim_*.sh files define lightweight wrapper functions that defer loading until first use. A built-in debug mode (DEBUG_DOTFILES) shows per-plugin timing with a braille spinner progress indicator during startup.
bootstrap.sh exports DOTFILES_DISTRO and provides is_macos / is_debian helpers that gate platform-specific behavior throughout the config.
Things you might want to steal or adapt.
Git workflow (shared/plugins/git.sh)
The most substantial plugin. Highlights:
| Alias | Command | What it does |
|---|---|---|
g |
git |
|
gs |
git status |
|
gap |
git add --patch |
Stage hunks interactively |
grp |
git reset --patch |
Unstage hunks interactively |
gcp |
git checkout --patch |
Discard hunks interactively |
gd / gds |
git diff / git diff --staged |
|
gr |
git log (10 recent) |
Compact colored log |
com |
Checkout main/master | Auto-detects the default branch |
cob / cow / cor |
Branch menus | All / unmerged / remote-only branches |
gup |
Pull + auto-prune | Cleans up merged branches |
gpushnew |
Push + set upstream | For new branches |
gbc |
Changed files on branch | Files changed since diverging from main |
gcm |
Commit with editor | Pre-fills a comment line, opens $EDITOR |
Short aliases (shared/core/aliases.sh)
Single-key commands for daily use, plus typo recovery:
v=vim l=ls o=open p=python k=ag af=ripgrep-file-search
cd..='cd ..' xx='cd ..' qq=l chmox='chmod +x'
Directory bookmarks (shared/plugins/diralias.sh)
favdir registers named cd aliases. Run it with no args in any directory to interactively save a bookmark to ~/.extras.rc.sh:
favdir code ~/projects/code # In your rc files
favdir # Interactive: saves current dir
favdirs # List all bookmarks
dir # Pick a bookmark from a menuFuzzy cd (shared/plugins/fuzzycd.sh)
Wraps cd with fzf-powered history matching. Type a partial directory name and it finds the best match from your visit history.
Editor routing (shared/plugins/edit.sh)
edit opens the current directory (or files) in whatever $EDITOR is set to. Paired with to_editor which pipes only_files into the editor — useful for chaining with git commands.
Spotify control (shared/plugins/spotify.sh)
m opens an interactive Spotify menu. mp play/pause, mn next, mprev previous — all show the current track.
Homebrew bundles (install/homebrew/)
Dependencies split by purpose for selective installation:
command-line.Brewfile— CLI tools (ripgrep, fzf, starship, gum, etc.)gui-apps.Brewfile— Desktop apps (Firefox, MacVim, 1Password, etc.)mac-app-store.Brewfile— App Store apps
Dotfiles lock/unlock (zsh/core/functions.sh)
Since .zshrc is symlinked from the repo, external tools (installers, language managers, etc.) sometimes silently append lines to it. The lock/unlock mechanism prevents this:
dotfiles lock # chmod a-w — makes .zshrc read-only
dotfiles unlock # chmod u+w — makes it writable again for intentional editsWhen locked, any tool that tries to modify .zshrc will fail with a permission error instead of silently polluting the file. As an extra safety net, .zshrc itself checks for uncommitted changes on every shell startup and warns you:
⚠ .zshrc has uncommitted changes! Run 'git -C ~/.dotfiles diff zsh/.zshrc' to inspect.
The actual shell configuration lives in zsh/zshrc-actual — .zshrc is kept as a thin, lockable shim that sources it.
Debug mode (bootstrap.sh)
When shell startup feels slow, enable debug mode to get per-plugin load times:
enable_debug_dotfiles # Creates ~/DEBUG_DOTFILES sentinel, reload to activate
rld # Reload the shell
disable_debug_dotfiles # Removes sentinelIn normal mode, the loader shows a braille spinner as plugins load. In debug mode, each plugin prints its load time:
+0.002s 1 shared:core ~/.dotfiles/shared/core/aliases.sh
+0.001s 2 shared:core ~/.dotfiles/shared/core/functions.sh
+0.045s 3 shared:plugins ~/.dotfiles/shared/plugins/01_shim_nvm.sh
...
This is what led to the 01_shim_*.sh lazy-loading pattern — the timing revealed that nvm, sdkman, and conda were each adding 200-500ms to shell startup.
Vim config (config/vim/)
Plugins managed as git submodules under config/vim/bundle/. Includes Pathogen, lightline, surround, vim-rails, ctrlp, and markdown support. Leader-key dark/light mode toggle.
.
├── bootstrap.sh # Loader core: dependency tracking, debug mode
├── setup.sh # Installer: symlinks, brew bundles, platform setup
├── Makefile # Thin wrapper around setup.sh
├── shared/
│ ├── core/ # Cross-shell aliases, functions
│ └── plugins/ # One file per tool (git, ruby, spotify, etc.)
├── zsh/
│ ├── .zshenv # Entry point, sets ZDOTDIR
│ ├── zshrc-actual # Main config, drives the plugin loader
│ ├── core/ # Zsh-specific core (paths, prompt, iterm2)
│ └── plugins/ # Zsh-specific plugins
├── bash/
│ ├── bootstrap.rc.sh # Bash entry point
│ ├── core/ # Bash-specific core
│ └── plugins/ # Bash-specific plugins
├── config/
│ ├── git/ # gitconfig, global gitignore
│ ├── vim/ # vimrc, gvimrc, plugins as submodules
│ ├── starship.toml
│ └── direnvrc
├── install/
│ ├── homebrew/ # Brewfiles (CLI, GUI, App Store)
│ ├── macos_install.sh
│ └── debian_install.sh
├── bin/ # Standalone scripts added to PATH
└── scratch.rc.sh # Scratchpad for trying new things