Personal machine setup: Homebrew catalog, manually-installed apps, macOS defaults, shell/git/ssh dotfiles, and vim.
git clone https://github.com/thewildpendulum/dotfiles.git ~/dotfiles
cd ~/dotfiles
./apply.sh --dry-run # preview everything first
./apply.sh--dry-run prints every action (brew bundle check, defaults.sh
contents, each dotfile copy, the vim clone) without installing or
writing anything. ./apply.sh --target-home DIR redirects the dotfile
copies to a scratch directory — but not the vim step, which always
targets the real $HOME (see Vim).
apply.sh restores macOS defaults, dotfiles, and the manual-install
checklist before Homebrew, so one unsatisfiable cask can't cost
you the rest.
Brewfile/apps-manual.md— installed brew packages and manually-installed apps../capture.shdiffs both against what's actually installed; update them by hand from its output. See Keeping capture.sh honest if it reports things you expect.defaults.sh/dump-defaults.sh— curated macOS defaults.dump-defaults.shprints current values for the tracked domains; add anything worth keeping todefaults.sh.dotfiles/—.zshrc,.gitconfig,.ssh/config, and select~/.config/*entries.gitconfigends in an[include]pointing at~/.gitconfig.netflix— harmless if that file doesn't exist (git silently skips a missing include), a hook for anything machine-specific you don't want committed here.zsh/— git completion and prompt sourced by.zshrc, copied to~/.zsh/byapply.sh.
apply.sh doesn't capture-and-copy a vim config — it rebuilds it from
source, the same way you'd do it by hand:
rm -f ~/.vimrc ~/.gvimrc ~/.vimrc.bundles; rm -rf ~/.vim
git clone https://github.com/thewildpendulum/vim_dotfiles.git ~/.vim
~/.vim/activate.shthewildpendulum/vim_dotfiles is a fork of braintreeps/vim_dotfiles
(kept as the upstream remote on the machine that has it cloned) —
push vim config changes there, not to a captured copy in this repo.
apply.sh also symlinks ~/.config/nvim/init.vim into the freshly
cloned repo, since activate.sh sets up .vimrc/.vim but not that.
This step always targets the real $HOME — activate.sh hardcodes
$HOME internally, so there's no way to redirect it to a scratch
directory. --dry-run still previews it safely (it only prints the
commands).
SSH keys: dotfiles/ssh_config's Host github.com block points
IdentityAgent at 1Password's SSH agent socket — no private key file
is ever generated or committed here. Sign into 1Password, create an
SSH Key item, enable Settings → Developer → "Use the SSH agent," and
add the item's public key to your GitHub account — otherwise you
can't push to this repo's own remote.
BetterTouchTool: sign into the same iCloud account, then enable Preferences → iCloud Sync. Config syncs automatically — no manual import needed.
Sublime Text: apply.sh restores the settings automatically — just
launch Sublime afterwards and Package Control reinstalls the listed
plugins on its own. Only the hand-edited settings files are versioned;
formatter.assets (~1MB of downloaded binaries) is not.
iTerm2: Settings → General → Preferences → check Load preferences
from a custom folder or URL and point it at ~/dotfiles/iterm2, then
pick Save changes: Automatically and restart iTerm. From then on
iTerm reads and writes its prefs straight out of this repo, so profile
and keybinding changes show up as commits instead of drifting.
Deliberately not scripted: iTerm rewrites its plist on quit, so a
defaults write issued from a shell running inside iTerm gets
clobbered. Two clicks in its own UI is the reliable path.
On a fresh machine this eats the captured plist. Turning the setting
on makes the running iTerm — still on stock defaults — write its state
into the folder before it ever reads what is there. The settings appear
not to have applied, and the file they were in is now overwritten too.
Recover from Terminal.app with iTerm fully quit (Cmd+Q), because iTerm
rewrites the file again the moment it exits:
cd ~/dotfiles && git checkout iterm2/com.googlecode.iterm2.plistRelaunch iTerm and the restored prefs load. After that the file showing as modified is expected — iTerm owns it and syncs continuously.
Caps Lock acts as Control. This is not done through System Settings →
Keyboard → Modifier Keys, even though that is the obvious place: macOS stores
that mapping per keyboard device, keyed by USB vendor and product id. It does
not follow you to a new machine, it does not apply to a keyboard you plug in
later, and dump-defaults.sh cannot capture it — it is a -currentHost
NSGlobalDomain key rather than one of the tracked domains. It was lost in
exactly that way during a laptop migration.
hidutil instead remaps at the HID level, for every keyboard at once, with no
extra software. The remap is cleared on reboot, so
launchagents/net.thewildpendulum.capslock-to-control.plist reapplies it at
login; apply.sh copies it into ~/Library/LaunchAgents and loads it.
To change or add a remap, edit the plist's key codes and rerun apply.sh
(it boots the agent out and back in, so edits take effect). Useful codes:
Caps Lock 0x700000039, Left Control 0x7000000E0, Escape 0x700000029,
Left Option 0x7000000E2, Left Command 0x7000000E3.
Apply it in the current session without waiting for a login:
launchctl kickstart "gui/$(id -u)/net.thewildpendulum.capslock-to-control"Undo it for the session with hidutil property --set '{"UserKeyMapping":[]}'.
What apply.sh sets up (detail lives in the named script/file):
- macOS defaults (
defaults.sh): Dock (no bounce, no auto-rearrange, hide recents, 64px icons, disabled bottom-right hot corner), Finder (show hidden files, list view, path bar), global (dark mode, show extensions, max trackpad speed, "omw" text replacement) .zshrc: oh-my-zsh + nvm + sdkman,md2docshelper. Everysourceis guarded so a missing framework degrades to a plain shell instead of erroring on every new terminal.- Shell frameworks: oh-my-zsh, nvm and sdkman are reinstalled by
apply.sh, not copied — each self-updates, and a copied install carries architecture-specific compiled state. sdkman runs after Homebrew, not with the other two: its installer needs Bash 4+ and macOS ships 3.2.zsh/(git completion + prompt) is hand-maintained, exists nowhere else, and is copied to~/.zsh/. .gitconfig: aliases, push/grep/core settings, credential helpers; includes~/.gitconfig.netflix.ssh/config:github.comvia 1Password's SSH agent (see SSH keys)- Vim: cloned fresh from
thewildpendulum/vim_dotfiles(see Vim) - LaunchAgents (
launchagents/): Caps Lock remapped to Control viahidutil(see Keyboard remapping) - BetterTouchTool: config via iCloud Sync, not scripted (see below)
- iTerm2 (
iterm2/): profiles, keybindings, appearance — iTerm loads prefs directly from this folder once pointed at it (see below) - Sublime Text (
sublime-text/User/): preferences + the Package Control package list, copied into place byapply.sh - Homebrew (
Brewfile) + manual installs (apps-manual.md)
capture.sh only reports things you can act on. Two kinds of false
alarm are filtered out:
Cataloged somewhere else. If another repo owns part of this
machine, point CAPTURE_EXTRA_CATALOGS at its catalog files
(colon-separated, any mix of Brewfile format and - Name lists) and
their entries stop showing up as NEW:
CAPTURE_EXTRA_CATALOGS=/path/to/other/Brewfile:/path/to/other/apps.md ./capture.shNothing about those catalogs is stored here — this repo stays self-contained, and the other repo's contents never leak into it. Add it to your shell profile if you use it every time.
Installed outside Homebrew. A cask in Brewfile that brew list
doesn't know about isn't necessarily missing — managed environments
install the same apps by other means. When the app bundle is present,
it's reported as "app present — installed outside Homebrew" rather
than MISSING.
For anything left over, .capture-ignore (one name per line, #
comments allowed) suppresses it. That file is gitignored, so
machine-specific noise never lands in the repo.
- Flat dotfiles (
~/.zshrc,~/.gitconfig,~/.ssh/config) are overwritten, but the previous file is first copied to<path>.bak.<epoch>. ~/.config/*entries are overlaid, not mirrored: files present in the destination but absent from this repo are left untouched. That's deliberate — some live config isn't meant to be committed here, and a rerun must not delete it.