skills/dotfiles-mac/SKILL.md
Create, update, or apply a macOS dotfiles repo. Use when the user wants to back up their system configuration, set up a new Mac from dotfiles, capture current configs into an existing dotfiles repo, or manage dotfiles with GNU Stow.
npx skillsauth add shhac/skills dotfiles-macInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Help users create, update, or apply a macOS dotfiles repo using GNU Stow and plain git.
~/.dotfiles/ (or user's existing dotfiles repo)--no-folding (always file-level symlinks, never directory-level)$HOMEos- (e.g., os-macos/) contain OS-specific files.local file pattern — gitignored files sourced/included by tracked configs~/.dotfiles/
├── setup.sh # Entry point: detects OS, delegates
├── .gitignore
├── .stow-local-ignore
├── README.md
│
├── # Cross-platform stow packages (each mirrors $HOME)
├── shell/ # → ~/.zshrc.shared, ~/.zprofile, etc.
├── git/ # → ~/.gitconfig, ~/.gitignore_global
├── ssh/ # → ~/.ssh/config (NOT keys)
├── gpg/ # → ~/.gnupg/gpg.conf, gpg-agent.conf
├── tmux/ # → ~/.tmux.conf or ~/.config/tmux/
├── nvim/ # → ~/.config/nvim/
├── ghostty/ # → ~/.config/ghostty/
├── claude/ # → ~/.claude/settings.json, skills, etc.
├── <tool>/ # Additional stow packages as needed
│
├── os-macos/ # macOS-specific (NOT auto-stowed)
│ ├── Brewfile # Homebrew packages/casks/taps/mas
│ ├── setup.sh # brew, Xcode CLT, stow, defaults
│ ├── defaults.sh # macOS defaults write commands
│ └── gpg/ # OS-specific override (e.g., pinentry-mac)
│
└── os-linux/ # Future: Linux-specific
├── setup.sh
└── gpg/ # Linux-specific override
The os- prefix keeps OS directories sorted together, visually distinct from stow packages.
For each stow package, check if os-{current_os}/ has a directory with the same name. If so, stow the OS-specific version instead of the common one — the OS version wins entirely.
Example: gpg/ has base config. os-macos/gpg/ has macOS-specific config (e.g., pinentry-program set to pinentry-mac). On macOS, only os-macos/gpg/ is stowed.
If OS-specific package directories contain files that should be ignored by stow (e.g., README.md), place a .stow-local-ignore in the os-macos/ directory — stow only reads this file from its -d directory.
Use the .local file pattern — tracked configs source/include an untracked .local counterpart:
~/.zshrc bootstrap (not stowed) → sources ~/.zshrc.shared then ~/.zshrc.local.zshrc.shared is tracked and stowed from shell/.zshrc.shared.gitconfig → [include] path = ~/.gitconfig.local.ssh/config → Include ~/.ssh/config.local at topAll .local files are gitignored. This avoids templating engines entirely.
You are helping a user manage their macOS dotfiles. Determine which workflow applies:
If unclear, ask the user which workflow they want.
Scan the user's machine to discover what's worth tracking. Run these in parallel where possible:
Existing dotfiles managers:
~/.local/share/chezmoi/), yadm (~/.local/share/yadm/), or bare git repos in $HOME (~/.cfg/, ~/.dotfiles.git/)Homebrew:
brew bundle dump --force --describe --file="$(mktemp /tmp/dotfiles-audit-Brewfile.XXXXXX)"
Shell configs:
~/.zshrc, ~/.zsh/, ~/.zprofile, ~/.zshenv, ~/.bashrc, ~/.bash_profile~/.config/fish/config.fish, ~/.config/fish/conf.d/, ~/.config/fish/functions/~/.oh-my-zsh/), Prezto, Starship, plain zsh~/.oh-my-zsh/custom/themes/ and custom plugins in ~/.oh-my-zsh/custom/plugins/ — these are user content worth tracking. Do NOT track OMZ core (it's managed by its own installer).Git:
~/.gitconfig (may contain [user] with name/email — fine to track)[includeIf] sections) — these reference paths that may need adjustment on other machines. Suggest moving [includeIf] blocks to ~/.gitconfig.local since they reference machine-specific paths~/.gitignore_global or equivalentSSH:
~/.ssh/config (track this)~/.ssh/id_*, ~/.ssh/*.pub, ~/.ssh/known_hosts, ~/.ssh/authorized_keysGPG:
~/.gnupg/gpg.conf, ~/.gnupg/gpg-agent.conf (track these)~/.gnupg/private-keys-v1.d/, ~/.gnupg/*.kbx, ~/.gnupg/trustdb.gpg, ~/.gnupg/openpgp-revocs.d/, ~/.gnupg/S.gpg-agent*Claude/AI configs:
~/CLAUDE.local.md, ~/.claude/settings.json, non-symlinked skills in ~/.claude/skills/~/.claude/auth/, ~/.claude/sessions/, ~/.claude/cache/, ~/.claude/telemetry/, ~/.claude/*.local.jsonTerminal emulator:
~/.config/ghostty/config~/.config/alacritty/alacritty.toml (current, since v0.13) or ~/.config/alacritty/alacritty.yml (legacy)~/.config/kitty/kitty.confEditor configs:
~/.config/nvim/~/.vimrc~/Library/Application Support/Code/User/settings.json, keybindings.json~/Library/Application Support/ (path with spaces). These can't be managed cleanly with stow — handle with direct symlinks in setup.sh instead:
ln -sf "$DOTFILES_DIR/vscode/.config/Code/User/settings.json" \
"$HOME/Library/Application Support/Code/User/settings.json"
Other common configs:
~/.tmux.conf or ~/.config/tmux/tmux.conf~/.config/starship.toml~/.ripgreprc~/.config/bat/config~/.config/ subdirectories for tools installed via Homebrew$XDG_CONFIG_HOME (default: ~/.config/). If set to a non-default path, use it as the stow target (-t $XDG_CONFIG_HOME) for packages that install into ~/.config/.macOS defaults:
NSGlobalDomain, com.apple.dock, com.apple.finder, com.apple.Safari, com.apple.screencapture, etc.Before proposing anything to track, scan discovered files for secrets:
token, api_key, secret, password, credential in config files~/.npmrc (may contain auth tokens) — detect and either exclude or template with placeholder~/.config/graphite/user_config (contains auth) — exclude~/.netrc — exclude~/.aws/credentials — exclude (but ~/.aws/config is safe)~/.docker/config.json (Docker registry auth) — exclude~/.kube/config (Kubernetes tokens/certs) — exclude~/.config/gh/hosts.yml (GitHub CLI OAuth tokens) — exclude~/.config/gcloud/ (Google Cloud credentials) — exclude~/.boto, ~/.s3cfg (S3 credentials) — exclude-----BEGIN.*PRIVATE KEY----- headers — this catches embedded private keys regardless of filenameexport statements where the variable name contains KEY, SECRET, TOKEN, PASSWORD, or CREDENTIAL — these often contain inline secrets.local file pattern to split themShow the user what was discovered, grouped by category:
## Discovered Configuration
### Homebrew (N formulae, N casks, N taps)
[summary of what's in the Brewfile]
### Shell (zsh + Oh My Zsh)
- .zshrc, .zprofile, .zshenv
- OMZ custom themes: [list]
- OMZ custom plugins: [list]
### Git
- .gitconfig (user: name <email>)
- .gitignore_global
### SSH
- config (N hosts configured)
- ⚠ Keys will NOT be tracked
### GPG
- gpg.conf, gpg-agent.conf
- ⚠ Secret keys will NOT be tracked
### [other categories...]
### ⚠ Excluded (secrets detected)
- ~/.npmrc (contains auth token)
- [other excluded files]
Ask the user:
~/.dotfiles/)Brewfile from the audit dump into os-macos/setup.sh (see Setup Script section below).gitignore covering:
id_*, *.key, *.pem, private-keys-v1.d/).npmrc, .netrc, auth tokens).local override files (*.local, .local/).dotfiles-backup/).DS_Store).stow-local-ignore (skip README.md, setup.sh, os-*, .git, .gitignore)README.md with repo overview and usage instructionsos-macos/defaults.sh~/.zshrc is local bootstrap (not stowed)shell/.zshrc.shared is stowed to ~/.zshrc.shared~/.zshrc.shared and ~/.zshrc.local.gitconfig includes [include] path = ~/.gitconfig.local.ssh/config includes Include ~/.ssh/config.local at topgit init, create initial commitAfter generating, ask if the user wants to apply the dotfiles now (stow them). If yes, run setup.sh with the stow subcommand.
The user has a dotfiles repo and wants to sync their current system state into it.
~/.dotfiles/, or ask)For each tracked category, compare current system files with repo contents:
Brewfile:
brew bundle dump --force --describe --file="$(mktemp /tmp/dotfiles-capture-Brewfile.XXXXXX)"
Then diff against the tracked os-macos/Brewfile. Show added/removed packages.
Config files: For each stow package, diff the target file against the repo copy. Show meaningful changes (ignore whitespace, comments-only changes are low priority).
New configs: Scan for config files that exist on the system but aren't tracked in any stow package. Suggest new packages.
Show the user a summary of what changed:
## Changes Since Last Capture
### Brewfile
- Added: package-a, package-b, cask-c
- Removed: old-package
### shell/.zshrc.shared
- [diff summary or key changes]
### New (untracked)
- ~/.config/ghostty/config (suggest: ghostty/ stow package)
### Unchanged
- git/, ssh/, gpg/
Ask the user which changes to apply to the repo.
chore: capture updated shell config and new packages)The user has a dotfiles repo and wants to apply it to a new or existing machine.
Execute setup.sh or walk through it step by step if the user prefers. See Setup Script section for the execution order.
If setup.sh fails partway through: the script uses set -euo pipefail so it stops on error. Some steps may have already completed (packages installed, some stow links created). Since each phase is idempotent, it's safe to fix the issue and re-run the script. Watch for stow conflicts or partial symlinks that may need manual cleanup before re-running.
After setup completes, present a next-steps checklist:
## Next Steps (manual)
- [ ] Import GPG secret keys: `gpg --import /path/to/private-key.asc`
Then set trust: `gpg --edit-key <KEY_ID>` → `trust` → `5` → `quit`
- [ ] Copy SSH keys to ~/.ssh/ and `chmod 600 ~/.ssh/id_*`
(or generate new: `ssh-keygen -t ed25519`)
(if using encrypted secrets with age, keys are already in place after decryption)
- [ ] Sign into Mac App Store (for `mas` packages in Brewfile)
- [ ] Authenticate services:
- [ ] `gh auth login` (GitHub CLI)
- [ ] `npm login` (npm registry)
- [ ] `gt auth` (Graphite)
- [ ] Create machine-specific overrides in ~/.zshrc.local, ~/.gitconfig.local, etc.
- [ ] Review and run macOS defaults: cd ~/.dotfiles && ./os-macos/defaults.sh
Ask me to help with any of these!
If the user wants to revert to their pre-stow state:
stow -D -d $DOTFILES_DIR -t $HOME <package> for each~/.dotfiles-backup/ exists, offer to restore backed-up filessetup.shThe root setup.sh detects the OS and delegates:
#!/usr/bin/env bash
set -euo pipefail
DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
case "$(uname -s)" in
Darwin) source "$DOTFILES_DIR/os-macos/setup.sh" ;;
Linux) source "$DOTFILES_DIR/os-linux/setup.sh" ;;
*) echo "Unsupported OS"; exit 1 ;;
esac
os-macos/setup.shSupports subcommands:
./setup.sh # Full install (all phases)
./setup.sh brew # Homebrew + brew bundle only
./setup.sh stow # Stow all packages only
./setup.sh macos # macOS defaults only
./setup.sh capture # Capture current state back to repo
./setup.sh restore # Un-stow all packages and restore backups
Phase 1: Foundation
1. Xcode Command Line Tools
- xcode-select -p &>/dev/null || xcode-select --install
2. Homebrew
- Detect arch: /opt/homebrew (ARM) vs /usr/local (Intel)
- Install if missing, eval brew shellenv
Phase 2: Packages
3. brew bundle install --file=os-macos/Brewfile --no-lock
- Non-fatal: individual failures warn but continue
Phase 3: Decrypt Secrets (if age-encrypted files exist)
4. Find all .age files in stow packages
- If none found, skip this phase
- Prompt for master passphrase once
- Decrypt each .age file to its non-.age counterpart
- Unset passphrase from environment after decryption
Phase 4: Frameworks
5. Oh My Zsh (if shell/ stow package uses it)
- Install if ~/.oh-my-zsh/ doesn't exist
- Use unattended mode and preserve local bootstrap: `CHSH=no RUNZSH=no KEEP_ZSHRC=yes ... --unattended --keep-zshrc`
6. NVM (if selected)
- Install with profile mutation disabled: `... | PROFILE=/dev/null bash`
Phase 5: Configuration
7. Ensure local ~/.zshrc bootstrap exists (not stowed)
- Local ~/.zshrc sources ~/.zshrc.shared and ~/.zshrc.local
- If an unmanaged ~/.zshrc exists, migrate it to ~/.zshrc.local before writing bootstrap
8. Stow all packages
- For each directory that isn't os-*, .git, or special files:
- Check for os-macos/ override → stow that instead if present
- Backup conflicting real files to ~/.dotfiles-backup/<timestamp>/
- stow --no-folding -d $DOTFILES_DIR -t $HOME <package>
- Exclude shell/.zshrc from stow (local bootstrap is intentionally unmanaged)
- Skip packages the user has excluded (via env var or config)
Phase 6: System Preferences (opt-in)
9. macOS defaults (only if explicitly requested or --with-defaults flag)
- Source os-macos/defaults.sh
- killall affected apps at the end (Dock, Finder, SystemUIServer)
Phase 7: Post-install
10. Change default shell to brew zsh (if not already)
- Ensure brew's zsh is in /etc/shells: sudo sh -c 'echo $(brew --prefix)/bin/zsh >> /etc/shells'
- Then: chsh -s $(brew --prefix)/bin/zsh
11. Print next-steps checklist
Before stowing, handle existing non-symlink files:
backup_if_needed() {
local target="$1"
if [ -L "$target" ]; then
# Existing symlink (possibly from another dotfiles manager)
local link_target="$(readlink "$target")"
echo " Replacing symlink: $target → $link_target"
rm "$target"
elif [ -e "$target" ]; then
local rel_path="${target#$HOME/}"
local backup_path="$BACKUP_DIR/$rel_path"
mkdir -p "$(dirname "$backup_path")"
mv "$target" "$backup_path"
echo " Backed up: $target → $backup_path"
fi
}
Every operation is safe to re-run:
defaults write is idempotent# Critical (stop): Can't install Homebrew, stow has unresolvable conflicts
# Non-critical (warn + continue): Individual brew packages, missing optional tools
# Since setup.sh uses set -euo pipefail, non-fatal sections must trap errors:
# brew bundle install ... || echo "⚠ Some packages failed (continuing)"
# stow ... || echo "⚠ Stow failed for $package (continuing)"
NEVER track or commit (unless encrypted with age — see Encrypted Secrets section):
.env files, environment secretsid_*, *.key, *.pem, *.p12, private-keys-v1.d/, *.kbx, trustdb.gpg, .env*ghp_, gho_, ghs_, github_pat_, sk-, npm_, xoxb-, xoxp-, xoxe-, AKIA, AIza, glpat-, pypi-, sk_live_, pk_live_, rk_live_, SG., dop_v1_token, secret, password values-----BEGIN.*PRIVATE KEY----- headersFor files with mixed content (safe config + embedded secrets):
.local overridetoken = <YOUR_TOKEN_HERE> # REPLACE with actual tokenAlways run a secret scan before git add — grep for token-like patterns in staged files.
This section is entirely optional. Users who don't want encryption skip it — the skill works exactly as before. Present this as a choice during Workflow A (Step 3).
age provides simple, modern file encryption using scrypt KDF and ChaCha20-Poly1305 (AEAD). Designed by Filippo Valsorda (Go security lead).
Install: brew install age
Security: scrypt KDF (adjustable work factor) → ChaCha20-Poly1305 authenticated encryption
Unlike transparent git encryption, age uses an explicit encrypt/decrypt model:
.age extension and ARE committed to gitsetup.sh finds .age files, prompts for password, decrypts them (strips .age extension), then stowsssh/
.ssh/
config # plaintext (stowed normally)
id_ed25519.age # encrypted (committed to git)
id_ed25519 # decrypted (gitignored, created by setup.sh)
# Encrypt a file
AGE_PASSPHRASE="pw" age -e -j batchpass -o file.age file
# Decrypt a file
AGE_PASSPHRASE="pw" age -d -j batchpass -o file file.age
Always use -j batchpass with the AGE_PASSPHRASE env var — never age -p (which is interactive/TTY only and unsuitable for scripting). The batchpass plugin ships with brew install age.
.age files).age files).npmrc with auth tokens CAN be tracked (as .age files)If using encrypted secrets, add the decrypted filenames to .gitignore (e.g., id_ed25519, private-keys-v1.d/). The .age versions stay tracked.
age -e -j batchpass, add .age extension. Add decrypted filenames to .gitignore. Commit .age files..age counterparts in the repo, prompt for password, re-encrypt current versions: AGE_PASSPHRASE="pw" age -e -j batchpass -o file.age file. Commit updated .age files.brew bundle (so age is installed), find all .age files, prompt for password once, decrypt each to its non-.age counterpart (see setup.sh integration below). Then stow as normal — stow sees the decrypted files.Add an age decrypt phase between brew bundle (Phase 2) and stow (Phase 4). Only runs if .age files exist in the repo:
# Phase 3: Decrypt secrets (if any)
age_files=$(find "$DOTFILES_DIR" -name '*.age' -not -path '*/.git/*')
if [ -n "$age_files" ]; then
echo "Encrypted secrets found. Enter master passphrase to decrypt."
read -sp "Passphrase: " AGE_PASSPHRASE; echo
export AGE_PASSPHRASE
for f in $age_files; do
age -d -j batchpass -o "${f%.age}" "$f"
echo " Decrypted: ${f%.age}"
done
unset AGE_PASSPHRASE
fi
-j batchpass — age -p prompts interactively on TTY and cannot be scripted. The batchpass plugin reads AGE_PASSPHRASE from the environment.unset AGE_PASSPHRASE when done to avoid leaking the passphrase to child processes.gitignore Template# Secrets & keys
id_*
*.key
*.pem
*.p12
*.pfx
private-keys-v1.d/
*.kbx
trustdb.gpg
openpgp-revocs.d/
secring.gpg
S.gpg-agent*
.npmrc
.netrc
.env*
known_hosts*
authorized_keys
random_seed
credentials
# Decrypted secrets (age)
# When using age encryption, the .age files are committed and
# decrypted counterparts are gitignored. Add specific filenames here:
# id_ed25519
# id_ed25519.pub
# private-keys-v1.d/*
# Machine-specific overrides (e.g., .zshrc.local, .gitconfig.local)
*.local
.local/
# Backups
.dotfiles-backup/
# OS artifacts
.DS_Store
.stow-local-ignore Template\.git
\.gitignore
\.stow-local-ignore
^README\.md
^setup\.sh
^os-.*
^LICENSE
development
Review a GitHub pull request using the passive, neutral, assertive, or aggressive profile, optionally paired with a named reviewer persona that sets the review voice, by statically reading the PR diff, metadata, comments, and discovered issue/context links to determine whether it solves the stated issue. Use for automated or manual PR review flows that should leave an emoji-marked top-level review plus targeted inline comments or suggestion blocks, without running code or blocking except for malicious-looking changes.
development
Audit a codebase's module boundaries — enumerate modules, map their seams (import edges between modules), produce a layered topology diagram, and classify each module as narrow, hub-by-design, or accidental hub (with separate flags for cycles, layer violations, and uncertain import graphs). Outputs a diagram plus a flagged-for-review list; does not change code. Use when assessing whether abstractions live at the right boundaries, before/after a refactor to verify the boundaries improved, or when an unfamiliar codebase needs an architectural map. Not for intra-module refactoring (see improve-code-structure), bug hunting, or feature work.
testing
Investigate and solve problems using a team of specialist agents. Use when facing complex, multi-faceted problems that benefit from parallel research and structured implementation.
tools
Sync a forked repository with its upstream. Fetches both remotes, shows divergence, resets shared branches to upstream, re-merges local-only branches, cleans up branches already merged upstream, and pushes. Use when upstream has accepted PRs or moved ahead and you need to bring your fork in line.