Personal macOS setup: shell, git, vim, AI agent preferences, macOS system preferences, and the Brewfile.
Config files live under topic directories and are symlinked into $HOME by
install.sh, so editing a file here edits the live config — there is no copy or
build step, and nothing to re-run after a change.
There is a private companion repo, private-dotfiles, holding the things that
should not be public: personal aliases, agent instructions, and bkp (the
backup command). Clone both side by side.
Order matters — each step assumes the previous one.
# 1. Both repos, as siblings. install.sh and .zshrc use these exact paths.
mkdir -p ~/projects/heckfer && cd ~/projects/heckfer
git clone git@github.com:Heckfer/dotfiles.git
git clone git@github.com:Heckfer/private-dotfiles.git
# 2. Install Homebrew, symlink the dotfiles, link bkp onto PATH, set zsh as
# the login shell.
cd dotfiles && ./install.sh
# 3. Open a new terminal (so .zprofile puts brew on PATH), then install the
# Homebrew packages: CLI helpers, plus the compile-time deps ASDF needs.
cd ~/projects/heckfer/dotfiles && brew bundle
# 4. Register the ASDF language plugins.
./configure.sh
# 5. macOS system preferences, then log out and back in.
./macos/set-defaults.sh
# 6. Claude Code skills.
claude plugins install mattpocock-skillsStep 1 needs git, which on a fresh machine means macOS will pop up the Xcode
command line tools installer. Accept it; install.sh checks for the tools again
and triggers the same installer if it is somehow still missing.
install.sh installs the Xcode command line tools (so git works) and Homebrew
(at /opt/homebrew, the path .zprofile expects), clones oh-my-zsh and its plugins, clones and imports the Dracula theme for Terminal.app,
symlinks the dotfiles, and runs editors/install-plugins.sh. It is idempotent —
re-running it is safe and is the way to pick up new entries. It finishes by
printing the GUI apps it cannot install for you plus a list of reminders;
read that output.
configure.sh only registers ASDF plugins; it pins no versions. Set those per
project with a .tool-versions file.
bkp (from the private repo, on PATH after install.sh) puts back what a
fresh install cannot recreate:
bkp list # what backups exist, and whether iCloud has them locally
bkp fetch # download any that iCloud has evicted
bkp restore claude # Claude Code transcripts, memories, prompt history
bkp restore sublime # editor tabs and unsaved buffers (quit Sublime first)
bkp restore macos # explains why this one is manualTake a backup before handing a machine over with bkp backup. Full
documentation is in the private repo's README.md.
| Path | Contents |
|---|---|
zsh/ |
.zprofile (login: Homebrew), .zshenv (all PATH exports), .zshrc (interactive: oh-my-zsh, plugins, ASDF) |
git/ |
.gitconfig and .gitignore_global |
vim/ |
.vimrc |
sh/ |
.utility_functions — small helpers sourced into every interactive shell |
ai/ |
AGENTS.md, the single source of truth for AI agent preferences |
macos/ |
set-defaults.sh and its notes — see macos/README.md |
editors/ |
VS Code extension list, Sublime package list, and install-plugins.sh which applies both |
keyboard/ |
Exported Keychron K12 Pro keymap. Data for the QMK/VIA configurator, not loaded by anything here |
Brewfile |
brew bundle installs it |
lefthook.yml |
Git hooks: shellcheck on staged shell scripts before each commit |
.zprofile— login shells only; initializes Homebrew..zshenv— every shell; owns allPATHexports. NewPATHentries go here, not in.zshrc..zshrc— interactive shells; oh-my-zsh,EDITOR, ASDF, and the sourcing of the alias and function files.
.zshrc sources .aliases from private-dotfiles by absolute path, so that
repo must be cloned to the path above or every new shell reports a missing file.
VS Code and Sublime Text are installed by hand, so editors/install-plugins.sh
skips whichever one it cannot find and is meant to be re-run once they are
present. install.sh calls it, or run it directly.
- VS Code —
editors/vscode-extensions.txtis one extension id per line, installed withcode --install-extension. This needs thecodecommand onPATH: in VS Code, run Shell Command: Installcodecommand in PATH from the command palette. Add an extension by appending its id; get ids for what you already have withcode --list-extensions. - Sublime Text —
editors/sublime/Package Control.sublime-settingsis symlinked into Sublime'sPackages/Userdirectory. Package Control reads itsinstalled_packageslist on launch and installs anything missing, so adding a package name there is the whole job. Package Control itself must be installed once by hand (Command Palette > Install Package Control), and Sublime has to have been launched at least once before thePackages/Userdirectory exists. The script also linkssublto~/.local/bin/sublime, which is onPATHand needs nosudo.
The Dracula theme for Terminal.app is cloned to ~/projects/external/dracula-terminal-app
(the external directory is for upstream repos that are not ours; install.sh
creates it). install.sh imports the profile with open and points
com.apple.Terminal's Default and Startup Window Settings at it. Terminal
rewrites its own preferences when it quits, so if install.sh was run from
Terminal those writes can be lost — quit and reopen Terminal and confirm the
profile is the default, setting it by hand if not.
ai/AGENTS.md is the source of truth, and the symlinks form a hub:
~/AGENTS.md -> ai/AGENTS.md
~/.claude/CLAUDE.md -> ~/AGENTS.md
~/.gemini/GEMINI.md -> ~/AGENTS.md
Every agent therefore reads identical content. Edit ai/AGENTS.md to change the
preferences for all of them.
Note the distinction: ai/AGENTS.md is global preferences for any project,
while the CLAUDE.md in this repo's root is guidance for working on this
repo. The latter is a symlink into private-dotfiles and is deliberately
untracked here.
Handy things that are easy to forget:
git cs # commit, prefixing [TASK-ID] parsed from the branch name
git delete-merged-branchesgit cs expects branches to look like TASK-123/short-description; it takes
the text before the first / as the task id.
Shell helpers from sh/.utility_functions include clean-phone (strip a phone
number to digits, into the clipboard), pair (open a VNC session to a host),
and start_vpn / stop_vpn.
- Shell scripts use
set -o errexit -o pipefail -o nounset. Keep that on new ones, and keep themshellcheck-clean. A lefthook pre-commit hook runsshellcheckon staged scripts; enable it once per clone withlefthook install(lefthook comes from the Brewfile). A new script without a.shextension needs its path added to theglobinlefthook.yml. - Indentation is 4 spaces.
- To register a new dotfile, add both the file and an
ln -sfline ininstall.sh. - Decide which repo a file belongs in before adding it. Client names, internal
hostnames, credentials, and anything describing the shape of private data go
in
private-dotfiles— this repo is public.