Ansible-based macOS environment setup with profile-based configuration management.
Automated macOS development environment setup powered by Ansible. One command installs Homebrew packages, symlinks dotfiles, configures SSH and Git per profile, manages secrets with Ansible Vault, and applies macOS system settings. The profile system lets you separate work, personal, and private configurations β and keep private profiles in their own git repos.
This repo ships with an opinionated set of packages and tools across four topical profiles (shell, neovim, development, macos-desktop). Fork it and make it yours β swap in your own packages, dotfiles, and profiles.
- Profile-based configuration β work, personal, private profiles that combine freely
- Homebrew, Cask, and Mac App Store automation
- Dotfile symlinking with XDG config directory support
- Per-profile SSH and Git configuration
- Secret management with Ansible Vault
- Private profiles as separate git repos β keep sensitive configs out of your public dotfiles
- CLI with shell completions for fish, bash, and zsh
- 18 modular Ansible roles β use what you need, ignore the rest
- macOS system settings automation
- Auto-bootstrapping β installs Homebrew, mise, uv, and Ansible on first run
- Prerequisites
- Quick Start
- Customization
- CLI Commands
- Profiles
- Available Tags
- Secret Management
- Architecture
- Project Structure
- Troubleshooting
- Contributing
- Acknowledgments
- License
- macOS Ventura (13) or later
- Xcode Command Line Tools:
xcode-select --install
Everything else β Homebrew, Python, Ansible, mise, uv β is installed automatically on first run.
-
Fork and clone:
git clone https://github.com/<your-username>/dotfiles.git ~/.dotfiles cd ~/.dotfiles
-
Configure profiles and settings:
./dotfiles config
-
Install everything:
./dotfiles install --all
After installation, dotfiles is available globally from any directory.
What just happened?
- Homebrew formulae and casks were installed
- Dotfiles were symlinked to your home directory and
~/.config/ - SSH config was generated from your profile settings
- Git config blocks were written per profile
- macOS system preferences were applied
- Shell was set (if configured)
- Mac App Store apps were installed (if
mastag was included) - The
dotfilesCLI was linked to~/.local/bin/dotfilesfor global access
This repo is designed to be forked and customized. The built-in profiles (shell, neovim, development, macos-desktop) contain the author's preferred tools β replace them with your own.
Edit the appropriate profile's config.yml β e.g., profiles/shell/config.yml for CLI tools, profiles/development/config.yml for dev tools, or profiles/{profile}/config.yml for profile-specific packages:
brew_packages:
- name: ripgrep
- name: fd
cask_packages:
- name: visual-studio-code
- name: firefox
mas_packages:
- name: Magnet
id: 441258766Then run: dotfiles install brew cask mas
Place files in the appropriate profile directory:
| Source | Destination | Method |
|---|---|---|
files/dotfiles/{file} |
~/.{file} |
Symlink |
files/dotfiles/config/{dir} |
~/.config/{dir} |
Symlink |
files/dotfiles-copy/{file} |
~/{file} |
Copy |
files/bin/{script} |
~/.local/bin/{script} |
Symlink |
All paths are relative to profiles/{profile}/. Then run: dotfiles install dotfiles
Create config.yml in the repository root to override any profile variables. This file is git-ignored, so your local tweaks won't affect version control.
| Command | Description |
|---|---|
dotfiles install [TAGS] |
Install packages and configure system |
dotfiles config |
Interactive profile and settings configuration |
dotfiles sync |
Pull, upgrade dependencies, push |
dotfiles upgrade |
Upgrade mise, Ansible Galaxy, Python packages |
dotfiles pull / push |
Git operations (main repo + profile repos) |
dotfiles edit |
Open dotfiles in $EDITOR |
dotfiles secret <cmd> |
Manage Ansible Vault secrets |
dotfiles completion <shell> |
Generate or install shell completions |
dotfiles profile list |
List all profiles with status and priority |
dotfiles profile bootstrap <name> |
Create a new profile |
dotfiles install --all # Everything
dotfiles install dotfiles brew # Specific tags
dotfiles install --profile shell,work brew # Specific profiles + tags
dotfiles install --sync --all # Sync before installing
dotfiles install -v # Verbose (-vv, -vvv for more)
dotfiles install --dry-run # Preview changes without applyingRun dotfiles <command> --help for full options on any command.
- Privacy: Keep work configs (corporate GitHub orgs, internal tools) in private repos
- Modularity: Separate concerns between different environments
- Portability: Share your main dotfiles publicly while keeping sensitive configs private
profiles/
βββ shell/ # Core CLI tools (priority 100)
β βββ config.yml
β βββ files/dotfiles/
βββ neovim/ # Editor configuration (priority 110)
β βββ config.yml
β βββ files/dotfiles/
βββ development/ # Dev tools (priority 120)
β βββ config.yml
β βββ files/bin/
βββ macos-desktop/ # GUI apps, fonts (priority 130)
β βββ config.yml
β βββ files/dotfiles/
βββ work/ # Work-specific configuration
β βββ config.yml
βββ personal/ # Personal configuration
β βββ config.yml
βββ private/ # Private profiles (git-ignored)
βββ mycompany/ # Separate git repo per company/context
βββ config.yml
βββ files/dotfiles/ # Profile dotfiles to symlink
βββ files/bin/ # Profile scripts
βββ tasks/main.yml # Custom Ansible tasks
βββ roles/ # Custom Ansible roles
βββ secrets/ # Vault-encrypted secrets
βββ .git/ # Managed as a separate git repo
# profiles/private/mycompany/config.yml
---
host:
name: mycompany-profile
priority: 200
brew_packages:
- name: internal-tool
ssh_client_config:
- host: "*.internal.company.com"
identity_file: ~/.ssh/company_key
remote_user: myuser
dotfiles profile bootstrap private/mycompany # Creates private profile with git repo
dotfiles profile bootstrap private/mycompany --no-git # Without git initialization
dotfiles profile bootstrap mycompany # Creates shared (public) profilePrivate profiles live in profiles/private/ (git-ignored) and can be managed as separate git repositories:
cd profiles/private/mycompany
git remote add origin git@github.com:you/dotfiles-mycompany.git
git push -u origin mainThe pull, push, and sync commands automatically discover and sync profile git repos.
See docs/profiles.md for nested profiles, custom tasks, and advanced configuration.
Run specific parts of the setup using tags:
| Tag | Description |
|---|---|
all |
Run everything |
brew |
Install Homebrew formulae |
brew-packages |
Install Homebrew formulae (alias for brew) |
cask |
Install Homebrew casks |
taps |
Configure Homebrew taps |
mas |
Install Mac App Store apps |
dotfiles |
Symlink dotfiles to home directory |
gitconfig |
Configure git (profile blocks) |
ssh |
Configure SSH |
python |
Python environment setup |
pip |
Install Python packages |
pipx |
Install pipx packages |
gem |
Install Ruby gems |
npm |
Install npm global packages |
composer |
Install PHP Composer packages |
docker |
Docker configuration |
mise |
Install mise-managed tools |
fonts |
Install fonts |
chsh |
Change default shell |
gh-extensions |
Install GitHub CLI extensions |
gh-repos |
Clone GitHub repositories |
mcp-servers |
Configure MCP servers for Claude |
coding-agents |
Configure coding agent tools |
cursor-cli |
Install Cursor CLI |
json-config |
Manage JSON configuration files |
yaml-config |
Manage YAML configuration files |
dotfiles secret init # Create global vault password
dotfiles secret init -p shell # Create profile-specific vault password
dotfiles secret set -p shell mcp.api_key # Set a secret (prompts for value)
dotfiles secret get -p shell mcp.api_key # Retrieve a secretSecrets are encrypted with Ansible Vault. Each profile can have its own secrets file and vault password.
See docs/secrets.md for the full reference including profile secrets, editing, rekeying, and more.
The system runs a four-play Ansible playbook:
- Gather Facts β collect system information (all hosts, linear strategy)
- Bootstrap β one-time setup: macOS settings, Homebrew installation (
localhost) - Per-Profile Setup β profile-specific tasks: dotfiles, pipx, MCP servers (all profile hosts)
- Finalize β aggregation across profiles: brew packages, SSH config, git config, etc. (
localhost)
Key design choices:
- Dynamic inventory β profiles are discovered automatically via a custom inventory plugin
- Aggregation pattern β the Finalize play collects variables from all profiles and merges them before executing once
- Mitogen β used as the connection strategy for faster execution
See docs/architecture.md for detailed architecture documentation.
βββ dotfiles # CLI wrapper script (entry point)
βββ playbook.yml # Main Ansible playbook
βββ packages/ # Python packages (UV workspace)
β βββ dotfiles_cli/ # Main CLI (Click-based)
β βββ dotfiles_profile_discovery/ # Shared profile discovery
β βββ symlink_dotfiles/ # Dotfile symlinking logic
βββ ansible_plugins/ # Custom Ansible plugins
β βββ inventory/ # Dynamic profile inventory
β βββ lookup/ # Aggregation lookup
β βββ filter/ # Custom Jinja2 filters
β βββ action/ # Custom action plugins
βββ profiles/ # Profile configurations
β βββ shell/ # Core CLI tools (priority 100)
β βββ neovim/ # Editor configuration (priority 110)
β βββ development/ # Dev tools (priority 120)
β βββ macos-desktop/ # GUI apps, fonts (priority 130)
β βββ work/ # Work-specific configuration
β βββ personal/ # Personal configuration
β βββ private/ # Private profiles (git-ignored, nested repos)
βββ roles/ # Ansible roles (18 modular roles)
βββ schemas/ # JSON schemas for config validation
βββ molecule/ # Integration tests
βββ docs/ # Documentation
βββ config.yml # Local overrides (git-ignored)
First run is slow
The initial run bootstraps all dependencies (Homebrew, mise, uv, Ansible, Python). Subsequent runs are much faster.
Homebrew permission issues
Run sudo chown -R $(whoami) /usr/local/ (Intel) or ensure /opt/homebrew/ is owned by your user (Apple Silicon).
How do I remove a package?
Set state: absent in your profile's config.yml and run the relevant install tag. Ansible will uninstall it for you.
How do I skip certain tags?
Use Ansible's skip-tags directly: mise x -- ansible-playbook playbook.yml --skip-tags mas,docker
How do I use this with an existing Homebrew setup?
It works out of the box. The installer only adds packages listed in your profiles β it won't modify or remove existing packages.
Dotfile conflicts
If a target file already exists and isn't a symlink, the dotfiles role will skip it. Back up or remove the existing file to allow symlinking.
Contributions are welcome. Please follow Conventional Commits for commit messages and run mise x -- uv run pre-commit run --all-files before submitting.
See CONTRIBUTING.md for the full guide.
- Jonas Friedmann β original fork source
- Ansible, Homebrew, mise, uv β the tools that make this work
