Skip to content

Latest commit

 

History

410 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mac Dev Machine Setup

Automated setup and maintenance for macOS development environments using Ansible. This repository helps you quickly set up a new Mac or keep your existing setup up-to-date with a single command.

Features

  • 🚀 One-command setup for new Macs
  • 🔄 Automated updates for all installed packages
  • 🎯 Separate profiles for personal and work environments
  • 🔐 Secure handling of API keys and private keys
  • Idempotent operations - run safely multiple times
  • 🛡️ Pre-flight validation and backup of existing configs
  • 📦 Comprehensive toolset including modern AI tools

Quick Start

New Mac Setup

  1. Clone this repository:

    git clone https://github.com/your-username/mac-dev-setup.git
    cd mac-dev-setup
  2. Run the bootstrap script:

    ./new-mac.sh
  3. Install everything:

    make        # For personal setup
    # OR
    make work   # For work setup

Existing Mac Updates

Update all installed packages:

make update

Available Commands

Command Description
make Complete personal setup (all tools + personal apps)
make work Complete work setup (essential tools only)
make update Update all installed packages
make check Dry run to preview changes
make permissions Verify/request the macOS permissions Homebrew needs (runs automatically before installs and updates)
make cli Install command-line tools only
make gui Install GUI applications only
make osx Configure macOS system preferences
make dock Configure dock items
make dotfiles Sync dotfiles from repository
make herdr Set up Herdr completely: state integrations, the agent skill, and the stowed config
make git Configure git identity, aliases, and GPG signing
make fonts Install developer fonts
make themes Install terminal themes
make app-store Install Mac App Store apps
make keys Install private keys (requires vault password)
make gpg Configure GPG signing for git (auto-detects key)
make gpg-setup Interactive YubiKey GPG key setup wizard
make node Install Node.js tooling only
make work-remove Remove work-only packages

What Gets Installed

Development Tools

  • Languages: Node.js (via nvm), Go, Rust, Python, Deno
  • Package Managers: Homebrew, npm, pnpm, yarn, cargo
  • Version Control: Git, GitHub CLI, Sourcetree
  • Containers: Docker, Colima, lazydocker
  • Databases: Redis Insight, TablePlus

Terminal & Productivity

  • Terminals: iTerm2, Alacritty, Ghostty
  • Shells: Zsh with Oh My Zsh, Starship prompt
  • Multiplexers: tmux, Zellij
  • Editors: Neovim, VS Code, Cursor
  • CLI Tools: fzf, ripgrep, bat, eza, zoxide, and more

AI & Modern Tools

  • AI Assistants: ChatGPT, Claude
  • AI Development: Ollama for local LLMs
  • API Testing: Bruno, Postman, HTTPie
  • HTTP Debugging: Proxyman
  • Transcription & Dictation: Talat, FluidVoice
  • Agent Workspace: Herdr, with per-agent state integrations (see below)

Herdr agent state

Herdr is a terminal workspace manager whose sidebar shows what each coding agent is doing — working, blocked, idle — which is what makes several concurrent agents legible at a glance.

Left alone, Herdr infers that state by pattern-matching the terminal screen, so it breaks whenever an agent changes how it renders. make herdr installs a per-agent integration instead, and the agent reports its own state over the Herdr socket. The agents covered are listed in herdr_integrations in defaults.yaml; run herdr integration install --help to see every supported target.

make herdr is meant to be the single command for Herdr, so it also:

  • installs the herdr agent skill for every agent in herdr_skill_agents, which lets an agent drive the workspace it runs inside — split a pane, run a command in it, read the output back, wait on a sibling agent. install-claude.sh installs this too as one of its external skill sources; both are idempotent and the overlap is deliberate, so make herdr does not depend on having run the full framework installer.
  • stows the herdr config, backing up a hand-written ~/.config/herdr/config.toml first, because stow refuses to overwrite a real file and aborts the whole invocation if it finds one.

The stow step needs the package to exist in ~/.dotfiles. A clone predating it is reported rather than treated as a failure — run make dotfiles to refresh the clone, which runs these same steps afterwards.

The hook scripts are versioned with the herdr binary, so they are installed by herdr integration install rather than checked into the dotfiles repo, where they would go stale on every upgrade. Herdr owns what it writes into ~/.claude/settings.json and ~/.codex/. Herdr's own configuration is a stow package in the dotfiles repo.

This runs after the dotfiles task on purpose: that task copies claude/.claude/settings.json over ~/.claude/settings.json, which drops the hook registration the Claude integration adds there. Running afterwards restores it on every converge.

Codex needs a one-time trust prompt (not automated, deliberately)

Installing the integration is not sufficient for Codex. Codex gates hooks behind a trust prompt, because a hook can run outside its sandbox. The first Codex launch after make herdr shows:

Hooks need review
Hooks can run outside the sandbox after you trust them.
  > Trust all and continue
    Review hooks
    Continue without trusting (hooks won't run)

Choose Trust all and continue. Codex records the decision in ~/.codex/config.toml:

[hooks.state."/Users/<you>/.codex/hooks.json:session_start:0:0"]
trusted_hash = "sha256:..."

Trusting is too late for the session you are in. The hook is a SessionStart hook, so it has already been skipped by the time you answer the prompt. Restart that Codex pane once more and the state reporting begins.

This step is deliberately left manual. The trusted_hash is a sha256 over an internal Codex representation of the hook — not over hooks.json, the hook script, the command string, or the JSON entry, all four of which were tested and none match — so Ansible cannot pre-seed it reliably. It would also change on every herdr integration bump and potentially on Codex releases. And pre-seeding it would defeat the point: the prompt is consent for a script to run outside the sandbox, which is a decision worth making rather than baking into a repo.

Claude Code has no equivalent gate; its hook works as soon as the session restarts.

Verify either agent with:

herdr agent list   # a reporting pane has a populated agent_session

System Enhancements

  • System Monitoring: Stats, glances, htop
  • App Cleanup: Pearcleaner (uninstalls apps and their leftover files)
  • Security: KnockKnock persistence scanner, SSH key management, GPG signing with YubiKey
  • Productivity: Raycast, Obsidian, Fantastical
  • Menu Bar Toolkit: Vorssaint (keep-awake, system monitor, per-app volume, clipboard history and more)

Git Configuration

The setup includes comprehensive Git configuration with productivity-enhancing aliases and modern diff tools.

Git Settings

The following Git configurations are automatically applied:

  • Editor: Neovim as default editor
  • Pull strategy: Rebase by default
  • Auto-prune: Fetch automatically prunes deleted remote branches
  • Auto-setup remote: Automatically sets upstream on push
  • Default branch: main for new repositories
  • Diff tool: Difftastic for syntax-aware diffs

Git Aliases

Basic Commands

  • git co - checkout
  • git br - branch
  • git ci - commit
  • git st - status
  • git last - show last commit
  • git unstage - unstage files
  • git lg - compact log with graph

Difftastic Integration

  • git dlog - log with difftastic diffs
  • git dshow - show commit with difftastic
  • git ddiff - diff with difftastic
  • git dl - log with patches using difftastic
  • git ds - short alias for dshow
  • git dft - short alias for ddiff

Productivity Aliases

  • git amend - amend last commit without editing message
  • git undo - undo last commit, keeping changes
  • git wip - quick work-in-progress commit
  • git cleanup [N] - interactive rebase last N commits (default: 10)
  • git fixup <commit> - create fixup commit
  • git recent - show recently worked branches
  • git aliases - list all configured aliases

Branch Management

  • git branches - show all branches with tracking info
  • git gone - list branches whose remotes are gone
  • git prune-branches - delete branches whose remotes are gone
  • git main - checkout the main/master branch

Stash Improvements

  • git stash-all - stash including untracked files
  • git pop - pop latest stash

History Exploration

  • git contributors - show contributor statistics
  • git graph - pretty graph of commit history
  • git today - show your commits from today
  • git yesterday - show your commits from yesterday

File Operations

  • git untrack <file> - stop tracking file
  • git ignored - show ignored files
  • git modified - list modified files only

Search and Blame

  • git find <text> - search commit messages
  • git who <file> - enhanced blame with move/copy detection

FZF-Powered Interactive Git Commands

These aliases provide an interactive interface using fzf (fuzzy finder) with live previews, making git operations more visual and intuitive.

Interactive Checkout & Navigation

  • git fco - Fuzzy checkout branch - Search and checkout any branch (local or remote) with commit preview
  • git fcoc - Checkout any commit - Browse entire commit history and checkout with preview
  • git fbr - Branch switcher - Switch between local branches with commit history preview
  • git ftag - Tag browser - Browse and checkout tags with full diff preview

Interactive File Operations

  • git fadd - Stage files interactively - Select files to stage with diff preview (supports multi-select with TAB)
  • git funstage - Unstage files - Select staged files to unstage with preview
  • git fdiff - Diff browser - Select modified files to diff interactively

Interactive Commit Operations

  • git fshow - Commit browser - Browse commits and show details with full diff preview
  • git flog - Log explorer - Browse and select multiple commits (returns commit SHAs)
  • git fcherry - Cherry-pick commits - Select commits to cherry-pick with preview (multi-select with TAB)
  • git ffix - Create fixup commits - Select target commit for fixup with preview
  • git frebase - Interactive rebase from point - Select commit to start interactive rebase from

Interactive Stash & Branch Management

  • git fstash - Stash browser - Browse stashes with full diff preview and apply selected
  • git fbrm - Delete branches - Select branches to delete (supports multi-select with TAB)

Interactive Search

  • git fgrep [pattern] - Grep in tracked files - Select file to search with bat preview, then grep for pattern

Tips for FZF aliases:

  • Use TAB to multi-select items where supported
  • Use Ctrl-C or ESC to cancel without making changes
  • Type to fuzzy search through the list
  • Arrow keys or Ctrl-J/K to navigate
  • Enter to confirm selection

Troubleshooting

Cask upgrades fail with "It seems there is already an App at …"

Some Homebrew versions wrote empty install receipts, which makes brew upgrade forget how to remove the old app before installing the new one. Repair the receipts (no downloads involved) and re-run the update:

scripts/fix-cask-receipts.py
make update

If the script reports casks from untrusted third-party taps, run the suggested brew trust <tap> command and re-run it.

Cask upgrades fail with "Failed to release … from quarantine"

macOS requires the App Management permission to modify app bundles in /Applications, and TCC permissions cannot be granted programmatically without MDM enrollment — that is by design. The setup automates everything that macOS allows: scripts/ensure-mac-permissions.sh runs before every install/update target, detects missing permissions with a harmless probe, clears any recorded denial, triggers the system prompt, opens the exact System Settings pane, and waits for the one-time grant. After you grant App Management and Automation (System Events) to your terminal once, every subsequent run is fully hands-off.

If an individual app still fails quarantine release after the permission is granted, its bundle likely carries a com.apple.macl attribute (check with xattr /Applications/<App>.app), which macOS never lets other processes modify. Fix with a clean reinstall: brew reinstall --cask <name>.

Detecting drift (apps installed outside the Brewfiles)

Apps installed ad hoc with brew install --cask are invisible to make update and won't exist on a freshly provisioned machine. To list everything installed that no Brewfile tracks (personal machine), run this from the repository root:

brew bundle cleanup --file=<(printf 'instance_eval File.read("Brewfile.common")\ninstance_eval File.read("Brewfile.personal")\n')

Piping the Brewfiles in on stdin doesn't work, because Brewfile.common loads its parts relative to its own location.

Nothing is removed without --force, and you shouldn't add it: the list also includes the fonts and dockutil, which Ansible tasks install outside the Brewfiles. For each other listed item, either add it to the appropriate Brewfile or add it to a removal list in defaults.yaml.

Customization

Modifying Package Lists

Most package inventory now lives in Brewfiles:

# Brewfile.cli
brew "your-favorite-cli-tool"

# Brewfile.gui
cask "your-favorite-app", greedy: true

# Brewfile.app-store
mas "Your App", id: 123456789

# Brewfile.personal
cask "personal-only-app", greedy: true

Brewfile.common aggregates the shared CLI, GUI, and App Store inventory. Brewfile.work is available for work-only additions and is currently empty.

How the Brewfiles are enforced

The Brewfiles are the source of truth, and every run makes the machine match them:

  • Missing apps are reinstalled, even when Homebrew still thinks they are installed. Deleting an app outside Homebrew leaves brew's install record behind, and brew bundle would skip it. Before each GUI bundle, scripts/clear-stale-cask-receipts.py clears such records so the install runs. Run it with --dry-run and a Brewfile to see which casks are affected.
  • Existing copies are overwritten. Bundles run with --force, so an app already in /Applications that Homebrew didn't install is replaced by the cask's copy.
  • Deleting an entry does not uninstall it. To remove a package, delete it from its Brewfile and add it to the matching removal list in defaults.yaml: cli_packages_to_remove_if_installed, gui_packages_to_remove_if_installed, or app_store_apps_to_remove_if_installed (by App Store id). Use gui_packages_to_zap_if_installed instead to also delete the app's settings and support files. A blanket brew bundle cleanup --force isn't used because it would also uninstall the fonts and dockutil, which Ansible tasks install outside the Brewfiles.
  • Apps outside Homebrew have their own task. Talat isn't on Homebrew or the App Store, so ansible/tasks/talat.yaml installs it from its release feed when it's missing, after checking it's notarized and signed by its developer. Talat updates itself from then on.

Using Your Own Dotfiles

Update the dotfiles_repo in defaults.yaml:

dotfiles_repo: https://github.com/yourusername/dotfiles.git

GPG Signing with YubiKey

This setup supports signing git commits with a GPG key stored on a YubiKey for enhanced security. Multiple YubiKeys with different keys are supported - git automatically detects which YubiKey is currently inserted and uses the correct key.

Prerequisites

  • YubiKey with OpenPGP support (YubiKey 5 series recommended)
  • GPG and YubiKey tools (installed automatically via make cli)

How It Works

GPG signing is automatically configured when you run make git (or make / make work, which include it). The setup:

  1. Imports GPG public keys from ansible/files/gpg/public-keys.asc into your local keyring (required for GPG to sign with YubiKey-stored private keys)
  2. Detects the signing key from the currently-inserted YubiKey (tries keyring first, falls back to parsing gpg --card-status fingerprint)
  3. Configures git with the detected key, enables commit signing, and installs the gpg-auto-sign wrapper script

First-Time Setup

  1. Install prerequisites:

    make cli

    This installs gnupg, pinentry-mac, and ykman.

  2. Set up your GPG key on YubiKey (if not already done):

    make gpg-setup

    This interactive wizard will:

    • Check if your YubiKey is connected
    • Detect existing GPG keys on the YubiKey
    • Guide you through generating a new key or importing an existing one
  3. Export your public key and add it to this repo:

    gpg --armor --export YOUR_KEY_ID >> ansible/files/gpg/public-keys.asc

    This ensures fresh machines can import the public key before configuring signing.

  4. Configure git to sign commits:

    make git

    This automatically:

    • Imports stored public keys into the local GPG keyring
    • Detects your GPG key ID from the inserted YubiKey
    • Configures git to use it for signing
    • Enables commit signing by default
  5. Add your public key to GitHub:

    gpg --armor --export YOUR_KEY_ID

    Copy the output and add it at: https://github.com/settings/keys

Daily Usage

After setup, git commits are automatically signed. The 8-hour cache means you'll enter your YubiKey PIN once per workday.

# Test signing works
git commit --allow-empty -m "Test signed commit"
git log --show-signature -1

# Verify YubiKey is detected
gpg --card-status

Backup YubiKey Setup

Important: Keys generated directly on a YubiKey cannot be extracted. Plan for backup BEFORE generating keys.

Option 1: Off-Card Backup During Generation (Recommended)

When running make gpg-setup and choosing to generate a new key:

  1. When asked "Make off-card backup of encryption key?", choose Yes
  2. GPG will create an encrypted backup file in ~/.gnupg/
  3. Store this backup securely (encrypted USB, password manager, safe)

To restore to a backup YubiKey:

# Import the backup key
gpg --import ~/.gnupg/sk_XXXXX.gpg

# Insert backup YubiKey
# Move the key to the new YubiKey
gpg --edit-key YOUR_KEY_ID
keytocard
# Select slot 1 for Signature, 2 for Encryption, 3 for Authentication
save
Option 2: Generate Keys on Computer First

For maximum flexibility with multiple YubiKeys:

# 1. Generate a master key on your computer (NOT on YubiKey)
gpg --full-generate-key
# Choose RSA (sign only), 4096 bits, set expiration

# 2. Add subkeys for signing, encryption, authentication
gpg --edit-key YOUR_KEY_ID
addkey  # Add signing subkey
addkey  # Add encryption subkey
addkey  # Add authentication subkey
save

# 3. Export backup BEFORE moving to YubiKey
gpg --armor --export-secret-keys YOUR_KEY_ID > master-key-backup.asc
gpg --armor --export-secret-subkeys YOUR_KEY_ID > subkeys-backup.asc
# Store these securely!

# 4. Move subkeys to primary YubiKey
gpg --edit-key YOUR_KEY_ID
key 1  # Select first subkey
keytocard
key 1  # Deselect
key 2  # Select second subkey
keytocard
# Repeat for all subkeys
save

# 5. For backup YubiKey, reimport and repeat
gpg --delete-secret-keys YOUR_KEY_ID
gpg --import subkeys-backup.asc
# Insert backup YubiKey
gpg --edit-key YOUR_KEY_ID
# Move keys to backup YubiKey using keytocard
Switching Between YubiKeys

With the same key on both YubiKeys:

# Remove cached key stub and re-detect
gpg-connect-agent "scd serialno" "learn --force" /bye

# Or fully reset the agent
gpgconf --kill gpg-agent
gpg --card-status

With different keys on each YubiKey:

This setup includes automatic key detection via a wrapper script (~/.local/bin/gpg-auto-sign). When you run make gpg, git is configured to use this wrapper which:

  1. Detects which YubiKey is currently inserted
  2. Reads the key ID from that YubiKey
  3. Uses that key for signing, regardless of what's in user.signingkey

This means you can simply swap YubiKeys and git will automatically use the correct key - no manual switching required.

Note: You'll need to add each key's public key to GitHub separately at https://github.com/settings/keys

Troubleshooting GPG

  1. "No secret key" errors:

    • Ensure your YubiKey is inserted
    • Run gpgconf --kill gpg-agent to restart the agent
    • Run gpg --card-status to verify YubiKey detection
  2. PIN prompt doesn't appear:

    • Ensure pinentry-mac is installed: brew list pinentry-mac
    • Check gpg-agent config includes pinentry-program
  3. Signing fails in terminal:

    • Ensure GPG_TTY is set: export GPG_TTY=$(tty)
    • Add this to your .zshrc if not already present

Validation and Safety

The setup includes several safety features:

  • Pre-flight checks: Validates system requirements before running
  • Backup creation: Backs up existing SSH keys before modification
  • Disk space check: Warns if disk space is low
  • Internet connectivity: Verifies connection before downloading
  • Dry run mode: Preview changes with make check

Troubleshooting

Common Issues

  1. "Homebrew not found" after new-mac.sh

    • Restart your terminal or run: source ~/.zshrc
  2. "Permission denied" errors

    • The makefile prompts once for the sudo password when needed
    • Homebrew cask installs use a temporary askpass helper so installers should not ask repeatedly
    • Some operations require admin access
  3. App Store apps fail to install

    • Ensure you're signed into the Mac App Store
    • Run mas signin your-apple-id@example.com first
  4. Ansible Galaxy certificate errors

    • We've removed the insecure ignore_certs setting
    • If you have certificate issues, fix your system certificates

Logs and Debugging

  • Run with verbose output: ansible-playbook local.yaml -vvv
  • Check specific task: make cli or make gui
  • Validate syntax: ansible-playbook local.yaml --syntax-check
  • Run the helper-script tests: python3 -m unittest discover -s scripts -p 'test_*.py'

File Structure

.
├── makefile              # Main interface for all commands
├── new-mac.sh           # Bootstrap script for fresh installs
├── Brewfile.*           # Homebrew Bundle package inventories
├── defaults.yaml        # Non-Brewfile configuration and removal lists
├── local.yaml           # Main Ansible playbook
├── update.yaml          # Update playbook
├── scripts/             # Helper scripts (e.g., gpg-auto-sign.sh)
├── ansible/
│   ├── files/           # Static files deployed to target machine
│   │   └── gpg/         # GPG public keys for import
│   ├── tasks/           # Individual task files
│   └── templates/       # Configuration templates

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Test your changes with make check
  4. Submit a pull request

Requirements

  • macOS (tested on macOS 15.0 arm64)
  • Internet connection
  • Apple ID (for App Store apps)

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

This setup is inspired by various dotfiles repositories and automation scripts from the developer community.

About

Scripts to automate the installation and setup of a new Mac, so I don't have to waste time

Resources

Stars

24 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages