Personal macOS (Apple Silicon) dotfiles. The repository is ~/.config —
there is no symlink farm or stow. Files live where the apps already look for
them (~/.config/<app>/...), and the shell is bootstrapped by a one-line
~/.zshrc that sources ~/.config/zsh/.zshrc.
| Path | Tool |
|---|---|
zsh/ |
zsh config, aliases, vendored plugins (submodules) |
starship/ |
prompt |
tmux/ |
tmux config + plugins (tpm) |
wezterm/, kitty/ |
terminal (kitty mirrors the wezterm look & keybinds) |
aerospace/ |
tiling window manager |
sketchybar/ + borders |
status bar + window borders |
helix/, neovide/, htop/, yazi-style configs |
misc tools |
brew/Brewfile |
the canonical package list (HOMEBREW_BUNDLE_FILE) |
Plugins are git submodules (zsh/plugins/*, tmux/plugins/*).
curl -fsSL https://raw.githubusercontent.com/fenghourun/dotfiles/main/install.sh | bashThe script (install.sh) is idempotent and:
- installs Homebrew if missing,
- makes
~/.configtrack this repo (backing up any existing~/.configto~/.config.backup-<timestamp>.tar.gzfirst), - initializes submodules,
- wires
~/.zshrc(source ~/.config/zsh/.zshrc) and~/.zprofile(brew shellenv), - runs
brew bundle, - starts the
sketchybarandbordersservices.
Then open AeroSpace.app and restart the terminal (exec zsh).
# Homebrew first (https://brew.sh), then:
git -C ~/.config init
git -C ~/.config remote add origin https://github.com/fenghourun/dotfiles.git
git -C ~/.config fetch origin main
git -C ~/.config checkout -f -B main origin/main
git -C ~/.config submodule update --init --recursive
echo 'source ~/.config/zsh/.zshrc' >> ~/.zshrc
brew bundle install --file=~/.config/brew/BrewfileBecause ~/.config is the working tree, day-to-day use is plain git.
Pull the latest config onto this machine:
cd ~/.config
git pull
git submodule update --init --recursive # in case plugins changed
brew bundle install --file=~/.config/brew/Brewfile # sync packages(Re-running bash ~/.config/install.sh does all of the above too.)
Push changes you made on this machine:
cd ~/.config
git add -p # review hunks
git commit -m "..."
git pushCapture newly installed Homebrew packages before switching machines
(optional — review the diff, since dump records every installed leaf,
including ones intentionally left out of the curated Brewfile):
brew bundle dump --force --file=~/.config/brew/Brewfile
git -C ~/.config diff brew/Brewfile # prune anything you don't want, then commitPlugins and the neovim config are git submodules, each pinned to a specific commit:
| Submodule | Repo |
|---|---|
nvim |
fenghourun/nvim |
zsh/plugins/* |
zsh-autosuggestions, zsh-syntax-highlighting, zsh-vi-mode |
tmux/plugins/* |
tpm |
Get / refresh them (also part of install.sh):
git -C ~/.config submodule update --init --recursivePull the latest upstream commit for a submodule and bump the pin here:
git -C ~/.config submodule update --remote nvim # fast-forward nvim to its latest master
cd ~/.config && git add nvim && git commit -m "bump nvim" && git pushEditing the neovim config (its own repo lives at ~/.config/nvim): commit
in the submodule first, then record the new pin in this repo:
cd ~/.config/nvim && git add -p && git commit -m "..." && git push # 1. push nvim changes
cd ~/.config && git add nvim && git commit -m "bump nvim" && git push # 2. bump the pin in dotfilesThe dev flow is ssh devserver → work inside one persistent tmux session
(neovim in one window/pane, shells in others). The session lives on the
server, so disconnects don't lose work — reconnect and you're back where you
left off.
- On an SSH login the shell auto-runs
tmux new-session -A -s main(seezsh/.zshrc), so you always land in the persistentmainsession. The guard only fires over SSH and when not already inside tmux — local macOS shells are unaffected. - Prefix is
C-a. Common keys:C-a |/C-a -split,C-a cnew window,C-a ddetach (leaves everything running),C-a h/j/k/lswitch panes,C-a+ arrows resize,C-a rreload. Neovim splits are navigated separately with the built-inC-w h/j/k/l. - Reattach manually after a detach/disconnect with
tmux attach -t main. - tpm finds its plugins via the
TMUX_PLUGIN_MANAGER_PATHenv var (set intmux/tmux.confto~/.config/tmux/plugins/), so no~/.tmuxsymlink is needed — the in-repo submodules just work.
Handing this to Claude on the devserver? Paste
DEVSERVER.md— it's a self-contained setup + usage brief for the remote.
Don't run install.sh here — it's macOS-only (Homebrew, sketchybar,
AeroSpace). On a remote where ~/.config may already contain unrelated,
pre-existing files, adopt the repo in place instead: this touches only the
files this repo tracks and leaves everything else alone (git never deletes
untracked files on checkout).
cd ~/.config
git init -q
git remote add origin https://github.com/fenghourun/dotfiles.git
git fetch origin main -q
git checkout -f -B main origin/main # overwrites only tracked files; anything else is untouched
git branch --set-upstream-to=origin/main main -q
git submodule update --init --recursive # tmux + zsh plugins
# Keep pre-existing files out of `git status` (and out of accidental commits) by
# marking everything currently untracked as locally excluded. .git/info/exclude
# is per-clone and never committed, so the repo itself stays clean.
git status --porcelain | sed -n 's/^?? //p' >> .git/info/exclude
# Wire the shell (only if not already sourced):
grep -qs 'source ~/.config/zsh/.zshrc' ~/.zshrc || echo 'source ~/.config/zsh/.zshrc' >> ~/.zshrcThen exec zsh (or reconnect) — the SSH auto-attach drops you into tmux.
To update later: cd ~/.config && git pull && git submodule update --init --recursive.
- Local / machine-specific files are git-ignored and never committed:
see
.gitignore(e.g.gh/hosts.ymlauth,git/, work devserver artifacts matching*.ash0). They stay on disk; git just doesn't track them. - Apple Silicon is assumed (
/opt/homebrew). On Intel, changeBREW_PREFIXininstall.shto/usr/local. - The zsh plugins here are vendored as submodules and sourced from
~/.config/zsh/plugins/; don't rely on the Homebrewzsh-*formulae.