Personal macOS configuration for khoi. Clone to ~/.dotfiles — the path is
hardcoded in zsh/zshenv, so anywhere else will not work.
git clone https://github.com/khoi/dotfiles.git ~/.dotfiles
cd ~/.dotfiles
./setup.shEverything is macOS-only and idempotent: re-running any script is always safe.
./setup.sh # default modules
./setup.sh --all # default + opt-in modules
./setup.sh vscode git # only the named modules, in the order given
./setup.sh --list # show what's availablesetup.sh installs Homebrew if missing, links the shell dotfiles into $HOME,
then runs each module's setup.sh.
| Modules | |
|---|---|
| default | git brew mise macos gpg iterm alfred ai misc |
| opt-in | vscode xcode terminal |
The opt-in ones need a GUI app that may not be installed, or only matter on some machines.
Two mechanisms, depending on whether the app reads a config file.
Symlinks — the repo file is linked into $HOME or an app's support
directory, so edits in the repo are live immediately.
| Repo | Linked to |
|---|---|
zsh/zshenv, zsh/zshrc |
~/.zshenv, ~/.zshrc |
git/gitconfig |
~/.gitconfig |
ssh/config |
~/.ssh/config |
curl/curlrc, tmux/tmux.conf, vim/vimrc |
~/.curlrc, ~/.tmux.conf, ~/.vimrc |
mise/global.toml |
~/.config/mise/config.toml |
ai/claude/* |
~/.claude/ |
ai/copilot/* |
~/.copilot/ |
vscode/{settings,keybindings}.json, vscode/snippets |
VS Code user dir |
Preference redirection — for apps with no dotfile.
macos/setup.shis a largedefaults writescript (mathiasbynens lineage; timezoneAsia/Ho_Chi_Minh, forced Dark mode). Needs sudo, and restarts Finder/Dock at the end.- iTerm2 is pointed at
iterm/via its "load preferences from a custom folder" setting. iTerm writes the plist back there on quit, so commit from the repo. - Alfred's
syncfolderis set toalfred/. - Terminal.app imports its theme through
osascript. gpg/setup.shcopiesgpg.confand a renderedgpg-agent.conf(arch-correctpinentry-macpath) into~/.gnupg. Copied, not symlinked — gpg insists on a real700directory. Keys andtrustdbare never versioned.- Stats (menu bar monitor) has its prefs imported by
misc/stats/setup.shfrommisc/stats/eu.exelban.Stats.plist; re-dump withbash misc/stats/update.sh.
zshenv → zshrc, which sources in this order:
zsh/env.zsh—$PATHand toolchain environment, built once and deduplicated withtypeset -U path.zsh/config.zsh— options, history, editor.zsh/omz.zsh— oh-my-zsh settings; must precede the plugin bundle.zsh/local/plugins.zsh— the antidote bundle.zsh/aliases.zsh,zsh/aliases.macos.zsh,zsh/functions.zsh,ai/aliases.zsh— after plugins, so these win.- mise, fzf, zoxide.
zsh/local/extra.zsh— machine-local, gitignored.
Plugins. Managed by antidote.
zsh/zsh.plugins is the list; zsh/local/plugins.zsh is the generated static
bundle, rebuilt automatically whenever the list is newer. To force a rebuild:
touch zsh/zsh.plugins && exec zshMachine-local state. zsh/local/ is gitignored and holds history, the
zcompdump, the generated bundle, and extra.zsh. Anything an installer wants
to append to ~/.zshrc belongs in zsh/local/extra.zsh instead.
Startup time. Roughly 350 ms. Profile it with:
ZSH_PROFILE=1 zsh -i -c exitThe two largest zprof entries — mise's hook and oh-my-zsh's library
sourcing — are inherent to those tools.
tmux/tmux.conf is set up for running several Claude Code agents at once —
one agent per pane, tiled and labelled, with layouts that survive a reboot.
Prefix is C-a.
| Binding | Action |
|---|---|
prefix | / prefix - |
split (keeps cwd) |
prefix Space |
tile all panes |
prefix A |
label the current pane (agent name, shown on its border) |
prefix S |
toggle synchronize-panes — type one prompt into every agent |
prefix b |
break a pane out to its own window |
prefix g |
scratch shell in a popup |
prefix s |
session switcher (one session per project) |
Plugins are managed by tpm, which
bootstraps itself on first launch (sensible, vim-tmux-navigator, yank,
resurrect + continuum). prefix I installs/updates them.
mise is the only version manager. It replaced pyenv, rbenv, jenv, nvm and asdf, which together cost ~2.5 s of every shell start.
mise ls # what's active
mise use -g node@22 # change a global pin
mise install # install everything pinned in mise/global.tomlGlobal pins live in mise/global.toml (python, node, ruby, go, java, and the
Swift tooling). Per-project .python-version, .ruby-version, .nvmrc,
.node-version and .java-version files are honoured, as are mise.toml and
.tool-versions. mise exports JAVA_HOME on its own, so no JDK is installed
through Homebrew.
brew/Brewfile is the single source of truth, grouped by formulae / casks /
fonts / mas.
sh brew/setup.sh # install
sh brew/setup.sh --cleanup # install, then uninstall anything not listed
sh brew/update.sh # dump what's installed, for reconciliation--cleanup is destructive and asks for confirmation. brew/update.sh writes
Brewfile.generated rather than overwriting the grouped Brewfile, so the
comment structure survives; fold in the diff by hand.
Both architectures are supported: every path decision keys off
$(uname -m) == arm64 → /opt/homebrew, otherwise /usr/local. Use
brew_prefix from lib/common.sh in new scripts rather than hardcoding
either.
On a new machine:
- Install the Xcode command line tools:
xcode-select --install. git clone https://github.com/khoi/dotfiles.git ~/.dotfiles && cd ~/.dotfiles./setup.sh— installs Homebrew, links dotfiles, runs the default modules.mise install— reinstall the pinned runtimes (nothing is copied across).- Sign in to the App Store, then re-run
sh brew/setup.shso themasentries install. - Import the GPG key and trust it — commits are signed
(
commit.gpgSign = true), so git will refuse to commit until this is done.git/setup.shwrites the correctgpg.programpath for the architecture andgpg/setup.shinstallsgpg.conf/gpg-agent.conf. gh auth login, thensh ai/setup.shfor the Copilot CLI extension../setup.sh --allif this machine needs VS Code or Xcode.
Each top-level directory is a self-contained module with a setup.sh that can
be run on its own. Scripts start with:
#!/usr/bin/env bash
set -euo pipefail
DOTFILES="${DOTFILES:-$(cd "$(dirname "$0")/.." && pwd)}"
. "${DOTFILES}/lib/common.sh"
require_macoslib/common.sh provides log/info/ok/warn/die, require_macos,
is_arm, brew_prefix, load_brew_shellenv, link (idempotent symlink with
backup) and module_dir.
.editorconfig governs formatting: 2-space indent, LF, final newline, trimmed
trailing whitespace — except in *.md and *.diff. C-family and Python use 4
spaces; Makefiles use tabs. Markdown is linted per .markdownlint-cli2.jsonc.
There is no build or test suite; this repo is shell scripts and config files.
Verify changes with bash -n (or shellcheck) and zsh -n.