Harden macOS setup: package tiers, drift detection, safer stow - #1
Merged
Conversation
The Brewfile had drifted badly from reality — `brew bundle check` failed on 7 of 13 entries, while ~20 formulae, 7 casks, and 15 App Store apps were installed and unlisted. Missing dockutil meant defaults.sh silently skipped its entire Dock section, and configs referenced packages the Brewfile never installed (git >= 2.38 for .gitconfig, ripgrep/fd for nvim, terminal-notifier for the alias). Split the inventory into Brewfile (core, every machine) and Brewfile.personal (media, games, creative). bootstrap.sh asks once whether this is a personal machine and records the answer in .machine (gitignored); --personal/--no-personal override it and unattended runs default to core-only. Two casks were renamed upstream: logi-options-plus -> logi-options+ and xcodes -> xcodes-app. Rebuild macos/defaults.sh around a single SETTINGS table walked by both apply and a new --check mode, so drift detection cannot fall out of sync with what gets written. Adds a Darwin guard, sudo keep-alive, and skips Dock entries whose app does not exist on this macOS version rather than aborting mid-run. New settings cover keyboard and text substitution (press-and-hold off so key repeat works in nvim), Finder, screenshots, and security — Touch ID for sudo via /etc/pam.d/sudo_local and the application firewall. FileVault is reported only, never enabled, since it generates a recovery key a human has to record. Extract login-item registration into macos/login-items.sh and use it for SizeUp, Mullvad VPN, Amphetamine, and Ice, retiring the manual System Settings step. A denied Automation prompt now warns instead of failing silently. Add doctor.sh: a read-only drift report across packages (both directions, tier-aware), stow links, macOS defaults, submodules, and environment. Exits 1 on drift. bootstrap.sh also gains flag parsing, an ERR trap (a mid-script failure previously exited with no output at all), an Xcode CLT check, nullglob around the prezto runcoms globs, and a fix for the brew shellenv fallback loop, whose trailing && chain made `set -e` kill the script when neither standard brew path existed. `brew bundle` failures are now non-fatal so a missing App Store sign-in no longer aborts before anything is stowed. Stow ssh with --no-folding, migrating ~/.ssh from a symlink into this repo to a real 700 directory containing only a symlinked config. As a folded symlink, any key or known_hosts written there — including by the ssh-keygen step the README recommends — landed in the git working tree. ssh is stowed first and alone so an unrelated conflict cannot leave ~/.ssh deleted between the unfold and the restow. Adopt the live ~/.claude/settings.json into the claude package. That file was a regular file, not a symlink, so the package had never actually been stowed and had diverged: it adds swift-lsp, "model": "opus", and an afplay notification hook in place of the cross-platform one. README updated accordingly. Expand CI with zsh syntax checks, plist linting, and a macOS job validating both Brewfiles, plus permissions, concurrency, and a SHA-pinned checkout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GtdF61oBMtp6qaMMWDTF73
CI's shellcheck flagged SC2015 on the stow check: `cd "$DOTFILES_DIR" && stow ... || true` swallows a failed cd as well as a failed stow, so an unreadable repo directory would have reported every package as fine. Pass --dir to stow instead, dropping the cd and the && chain entirely. The rule never fired locally: ubuntu-latest ships an older shellcheck whose SC2015 heuristic differs from 0.11.0, so a locally-clean script failed in CI. Pin the CI version to match what the Brewfile installs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GtdF61oBMtp6qaMMWDTF73
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
The Brewfile had drifted badly from reality:
brew bundle checkfailed on 7 of 13 entries, while ~20 formulae, 7 casks, and 15 App Store apps were installed and unlisted. Missingdockutilmeantdefaults.shsilently skipped its entire Dock section, and several configs referenced packages the Brewfile never installed.Three latent failure modes also needed closing — details below.
Changes
Package tiers.
Brewfile(core, every machine) +Brewfile.personal(media, games, creative).bootstrap.shasks once whether this is a personal machine and records the answer in.machine(gitignored);--personal/--no-personaloverride it, unattended runs default to core-only. Two casks were renamed upstream:logi-options-plus→logi-options+,xcodes→xcodes-app.macos/defaults.shrebuilt around a declarative table. OneSETTINGStable walked by both apply and a new--checkmode, so drift detection cannot fall out of sync with what gets written. Adds a Darwin guard, sudo keep-alive, and skips Dock entries whose app doesn't exist on this macOS version instead of aborting mid-run. New coverage: keyboard and text substitution (press-and-hold off so key repeat works in nvim), Finder, screenshots, and security — Touch ID forsudovia/etc/pam.d/sudo_local, and the application firewall. FileVault is reported only, never enabled, since it generates a recovery key a human has to record.macos/login-items.sh. Registration extracted and reused for SizeUp, Mullvad VPN, Amphetamine, and Ice, retiring the manual System Settings step. A denied Automation prompt now warns instead of failing silently.doctor.sh. Read-only drift report across packages (both directions, tier-aware), stow links, macOS defaults, submodules, and environment. Exits 1 on drift.CI. zsh syntax checks, plist linting, and a macOS job validating both Brewfiles, plus
permissions,concurrency, and a SHA-pinned checkout.Failure modes fixed
bootstrap.shcould die silently. The brew-shellenv fallback loop's trailing&&chain madeset -ekill the script with no output when neither standard brew path existed. Added anERRtrap, an Xcode CLT check,nullglobaround the prezto globs, and non-fatalbrew bundleso a missing App Store sign-in no longer aborts before anything is stowed.~/.sshwas a symlink into this repo. Stow had folded the whole directory, so any key orknown_hostswritten there — including by thessh-keygenstep the README recommends — landed in the git working tree. Now stowed--no-foldingas a real 700 directory containing only a symlinkedconfig.sshis stowed first and alone so an unrelated conflict can't leave~/.sshdeleted between the unfold and the restow.claudepackage had never been stowed.~/.claude/settings.jsonwas a regular file that had diverged; adopted into the repo, which addsswift-lsp,"model": "opus", and anafplaynotification hook in place of the cross-platform one.Verification
shellcheck,zsh -n,plutil -lint, andbrew bundle listall clean. Tier resolution verified across all three paths (flag / recorded / non-tty default), as was the~/.sshmigration and the non-fatalbrew bundlepath.defaults.sh --checkanddoctor.shboth correctly report the machine's outstanding drift and exit 1.Two settings are best-effort and may still show as drift after applying — the screensaver password keys (partly system-managed since Ventura) and
socketfilterfw(deprecated by Apple). Both are flagged in comments.No packages were installed and no system defaults applied on the local machine; that's left to run deliberately.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GtdF61oBMtp6qaMMWDTF73