- macOS (Apple Silicon or Intel)
- Xcode Command Line Tools:
xcode-select --installFollow these steps in order on a new machine:
# 1. Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 2. Install Ansible
brew install ansible
# 3. Clone this repo
git clone https://github.com/your-user/devops-daily-driver.git
cd devops-daily-driver
# 4. Install Ansible Galaxy collections
ansible-galaxy collection install -r requirements.yml
# 5. Configure your machine variables
cp host_vars/localhost.yml host_vars/localhost.yml.bak
# Edit host_vars/localhost.yml with your settings
# 6. Run the playbook
ansible-playbook daily-driver.ymlThen, by hand, once the first run has finished (these steps are tagged never, so a
normal run skips them, and they need apps the first run installs):
# 7. Log in to Bitwarden once (the dotfiles `bw` wrapper saves the session)
bw --regenerate-session-key
# 8. Import Raycast settings from Bitwarden, then grant the privacy permissions
ansible-playbook daily-driver.yml --tags raycast-import
ansible-playbook daily-driver.yml --tags permissionsThe playbook prompts once for a sudo password. It is used by casks that ship their
own installers (the Logitech ones), to enable Touch ID for sudo and to read the
Bitwarden session for the Raycast import. Press enter to skip it: those tasks are
skipped or fail while everything else proceeds. -K is not needed.
Preview a run without changing anything:
ansible-playbook daily-driver.yml --checkdevops-daily-driver/
├── .github/workflows/ # CI: yamllint, ansible-lint, syntax-check
├── .ansible-lint # Rule config, with deferred rules documented
├── .yamllint # Line length, ignores .ansible/
├── ansible.cfg # Inventory path, deprecation_warnings
├── inventory.yml # Explicit localhost (avoids Ansible warnings)
├── daily-driver.yml # Main playbook
├── requirements.yml # Galaxy collections
├── host_vars/
│ └── localhost.yml # Machine-specific variables (cloud providers, etc.)
└── roles/
├── system/ # preflight, directories, dotfiles
├── macos/ # dock, finder, keyboard shortcuts
├── apps/ # ui-apps, cli-apps, docs, networking
├── dev-tools/ # git, gnupg, golang, cicd, kubernetes, iac
├── virtualization/ # docker, colima, buildx (Homebrew)
├── editors/ # vscode, jetbrains, neovim, claude code, rtk
├── terminal/ # ghostty, zsh, tmux, fonts, nvim
├── aws/ # awscli, eksctl, granted
├── azure/ # azure-cli
├── gcp/ # google-cloud-sdk
└── custom-tools/ # custom taps and tools
# Only macOS settings
ansible-playbook daily-driver.yml --tags macos
# Only CLI apps
ansible-playbook daily-driver.yml --tags cli-apps
# Only dotfiles
ansible-playbook daily-driver.yml --tags dotfiles
# Only the AeroSpace setup (WM, borders, SketchyBar, fonts, macOS tweaks, dotfiles,
# then the interactive permissions guide)
ansible-playbook daily-driver.yml --tags aerospace
# Only the permissions guide (e.g. after `brew upgrade sketchybar`)
ansible-playbook daily-driver.yml --tags permissions
# New Mac: restore the per-machine files saved with `local-backup` (dotfiles)
# from Bitwarden; existing files are kept. Another machine's backup:
# -e local_files_machine=Old-Mac
ansible-playbook daily-driver.yml --tags local-restore
# Homebrew packages installed by hand that the playbook doesn't know about
# (leftovers to uninstall, or tools to add to a role)
scripts/brew-drift.shTags come in two layers. Role and area tags are the ones you normally reach for:
system, preflight, directories, dotfiles, macos, dock, finder,
keyboard, apps, ui-apps, cli-apps, logitech, networking,
documentation-tools, dev-tools, git-tools, gh-cli, glab, golang,
cicd, kubernetes, cluster-visualization, iac, terraform, opentofu,
secrets, ai, linters, virtualization, colima, docker-buildx, editors,
terminal, ghostty, zsh, pyenv, pipx, nvm, fonts, nvim, aws, azure,
gcp, custom-tools, demo, menubar, node, ios, krr.
Feature tags cut across roles: aerospace runs everything for the window
manager setup (AeroSpace, borders, SketchyBar, its fonts, macOS Spaces/menu bar
tweaks, dotfiles sync) and ends with the interactive permissions guide.
Individual packages also carry their own tag (bat, jq, htop, k9s, …), so
you can install a single tool without running its whole role:
ansible-playbook daily-driver.yml --tags k9sMachine-specific variables live in host_vars/localhost.yml. Role-specific defaults are in each role's defaults/main.yml and can be overridden via host_vars.
Key variables to configure in host_vars/localhost.yml:
# Cloud providers (opt-in, default false)
install_aws: true
install_azure: false
install_gcp: false
# Optional tools (opt-in, default false)
install_kubelogin: false # OIDC login plugin for kubectl
install_node: false # yarn, corepack
install_ios: false # xcodegen, ldid, zsign, dpkg (sign iOS apps outside the App Store)
install_tfsec: false # Terraform security scanner (trivy config covers it)
install_helmfile: false # declarative helm releases (ArgoCD deploys the charts instead)
# Dotfiles
dotfiles_repo: "https://github.com/your-user/dotfiles.git"
# Directories to create in $HOME
directories_to_create:
- personal
- work
# Custom tools (tap + formulae)
custom_user_taps:
- your-tap/tap
custom_user_tools:
- your-tap/tap/tool-name
custom_user_casks:
- your-tap/tap/app-nameCloud provider roles are opt-in and default to false. Enable them in host_vars/localhost.yml or override on the fly:
ansible-playbook daily-driver.yml -e install_aws=trueCombine with tags to run a single provider:
ansible-playbook daily-driver.yml --tags aws -e install_aws=trueClones the repository defined in host_vars/localhost.yml (macOS only), syncs .config to
~/.config, copies .zshrc and .p10k.zsh to ~, and copies any of .gitconfig,
.gitconfig-personal, .gitconfig-work if present. Syncing .config reloads tmux,
AeroSpace, borders and SketchyBar.
~/.zshrc is overwritten on every run. Anything specific to one machine (work-only
tools, aliases) belongs in ~/.zshrc.local, which .zshrc sources last and the
playbook never touches. Shared shell functions live in dotfiles .config/zsh/*.zsh and
are sourced automatically (e.g. bw.zsh, the Bitwarden CLI wrapper, which needs
export BW_USER=<email> in ~/.zshrc.local). Tokens and API keys are not stored in
any file: they are fetched from Bitwarden when the tool that needs them starts (see the
Secrets section of the dotfiles README).
Shell startup stays around 0.2 s: nvm loads on first use, pyenv init skips the rehash
and tool completions are cached in ~/.cache/zsh (regenerated when the tool is upgraded).
Other per-machine files, also never overwritten:
| File | Purpose |
|---|---|
~/.zshrc.local |
machine-specific shell config (no tokens; see above) |
~/.config/tmux/sessions.local |
repos preloaded as tmux sessions |
~/.config/sketchybar/aws_profiles |
AWS profiles shown in SketchyBar |
To update a machine without running Ansible, run dotfiles-sync (a shell function
from dotfiles .config/zsh/dotfiles.zsh): it pulls ~/dotfiles, syncs the same files
as the dotfiles tag and reloads tmux, AeroSpace, SketchyBar and borders.
The macos role configures:
-
Dock — size, position (right), magnification, animation, indicators; bottom-right hot corner (Quick Note) off
-
Finder — extensions, hidden files, path bar, status bar, column view; no
.DS_Storeon network shares or USB drives -
Appearance — dark mode
-
Menu bar — Sound always shown
-
Spaces — no automatic rearranging of Spaces; "Displays have separate Spaces" off (AeroSpace recommends it; needs a logout); clicking the wallpaper only reveals the desktop in Stage Manager
-
Screenshots — saved to
~/Pictures/Screenshots, no window shadow -
Keyboard — full keyboard access, no smart quotes or dashes, Spotlight shortcuts disabled, Siri disabled; with AeroSpace, Mission Control ctrl ←/→ disabled
-
Touch ID for sudo —
pam_tid.soin/etc/pam.d/sudo_local(survives macOS updates), preceded bypam_reattach(Homebrewpam-reattach) so it also works inside tmux. Needs the sudo password asked at the start of the run; skipped when left empty. Run alone with--tags touchid.Both Spotlight and Siri bind ⌘Space; Siri's default is hold ⌘Space, which swallows the keystroke before any app sees it. The playbook frees the shortcut but cannot set Raycast's hotkey — Raycast keeps it in an encrypted internal database with no supported
defaultskey. See Manual steps.
Installs AeroSpace (nikitabobko/tap/aerospace).
Config and cheat sheet live in dotfiles .config/aerospace/. ⌥f is left free for Raycast's
AeroSpace extension (see Raycast). The cli-apps
tasks check with pgrep whether it is running and start it if not, so "running"
is the desired state rather than a side effect of the install task. Syncing
dotfiles .config reloads it through handlers.
JankyBorders (FelixKratz/formulae/borders)
highlights the focused window; its config lives in dotfiles .config/borders/bordersrc.
SketchyBar (FelixKratz/formulae/sketchybar) is a
bottom status bar: AeroSpace workspaces with app icons, focused app, Spotify, Slack, kube
context (red for prod), Colima profiles, GitHub/GitLab reviews, AWS/GCP sessions,
Slack/Discord badges, CPU, RAM, weather and outdated Homebrew packages. Config lives in dotfiles .config/sketchybar/;
fonts (font-sketchybar-app-font, font-sf-pro, font-hack-nerd-font) are installed by the fonts and aerospace tags.
The GitLab, AWS and GCP items stay hidden until their CLI is logged in.
Installs Ghostty (tip/development build); its config comes from the dotfiles .config sync.
Every Ghostty window starts inside tmux (.config/ghostty/tmux-start.sh in
dotfiles): Ghostty's native tabs are separate macOS windows that AeroSpace tiles,
so tabs, splits and sessions come from tmux instead, driven by the usual ⌘
shortcuts (⌘T, ⌘⇧←/→, ⌘1–9, ⌘D, ⌘W, ⌘⇧S for sessions). The first window attaches
the persistent main session; extra windows get throwaway sessions. ⌘O opens any repo
under ~/work as a session, ⌘G opens lazygit in a popup, ⌘⇧G a side-by-side diff of uncommitted changes
(lumen, from custom_user_tools), ⌘E the yazi file
manager, ⌘⇧L lazydocker, ⌘K k9s and ⌘J a persistent scratch terminal. Sessions survive reboots
via tmux-resurrect + tmux-continuum, cloned by the playbook at pinned commits
(tmux_plugins in roles/terminal/defaults/main.yml) into ~/.config/tmux/plugins. Config:
dotfiles .config/tmux/tmux.conf. tmux is installed by the terminal and
aerospace tags.
Installs neovim via Homebrew. Config is managed via dotfiles.
The dev-tools role installs gnupg and pinentry-mac and writes
~/.gnupg/gpg-agent.conf, so GPG signing works if you already carry a key.
If you keep your SSH keys in an agent (1Password, Bitwarden, ssh-agent), SSH
signing is less work than maintaining a GPG keyring — git has supported it since
2.34 and there is no key material on disk:
# ~/.gitconfig-personal
[user]
email = <id>+<user>@users.noreply.github.com
signingkey = key::ssh-ed25519 AAAA...
[gpg]
format = ssh
[commit]
gpgsign = trueThe key:: prefix means "this is the key itself", not a path — git hands the
public key to the agent, which signs with the private half. Register the same
public key on GitHub a second time under Settings → SSH and GPG keys, with key
type Signing Key; a key registered only for authentication will not verify
signatures.
git log --show-signature reports No signature until you also point
gpg.ssh.allowedSignersFile at a file mapping emails to keys. That is a local
verification concern only — GitHub verifies against the keys on your account.
Raycast keeps its settings in a SQLCipher-encrypted database and exposes no
deeplink, CLI or defaults key to configure it, so nothing about it can be set
declaratively. Two things follow.
The hotkey stays manual — set ⌘Space in Settings → General → Raycast Hotkey. The playbook frees the shortcut by disabling the Spotlight and Siri bindings, but cannot assign it.
Everything else can travel in a .rayconfig export (Settings → Advanced → Export
Settings & Data), which covers extensions and their preferences, hotkeys,
quicklinks, snippets, window layouts and general settings. Keep it as an
attachment (any *.rayconfig) on a Bitwarden item (raycast_bitwarden_item, here
"Raycast Backup", with the Export Passphrase as its password); the playbook downloads it to a private temporary file,
opens the import and deletes the copy once you confirm:
ansible-playbook daily-driver.yml --tags raycast-import # give the sudo passwordThe Bitwarden session is the one saved by the dotfiles bw wrapper (read as root
with the sudo password; run bw --regenerate-session-key once if it has none). An
exported BW_SESSION takes precedence.
After changing Raycast, export again and replace the attachment (Bitwarden app, or
command bw create attachment --file <export> --itemid <id> after deleting the old
one). Without raycast_bitwarden_item, a local raycast_config_path is used instead.
The tasks carry the never tag, so ordinary runs skip them — otherwise every run
would pop Raycast's import dialog. Run them by hand after the first full run (see
Fresh install): they need Raycast and bitwarden-cli, which that
run installs, and a Bitwarden login. Import still asks for confirmation; this makes
it one click instead of reconfiguring extension by extension.
Keep the export out of a public repo. It is an opaque blob that may carry extension credentials, so secret scanners like gitleaks will not catch them. Store it in a password manager or private location. Raycast Pro's Cloud Sync (
requiresPro) removes the need for the file entirely.
Window search: install AeroSpace Tiling Window Manager from the Raycast Store, set
its Default Search preference to All, and give Switch Apps in Workspace the
hotkey ⌥f (AeroSpace leaves it unbound). ⇧↩ pulls the window to the focused
workspace, ↩ jumps to it. A .rayconfig export carries all of this.
macOS only lets the user grant these. --tags permissions (also part of
--tags aerospace, never part of a full run) opens the right pane, reveals the
sketchybar binary in Finder and waits for Enter.
| Where (System Settings → Privacy & Security) | App | Why |
|---|---|---|
| Accessibility | AeroSpace (/Applications/AeroSpace.app) |
move and tile windows |
| Accessibility | sketchybar (/opt/homebrew/Cellar/sketchybar/<version>/bin/sketchybar) |
read Slack/Discord Dock badges |
| Automation → System Events | sketchybar (prompted on first use) | Dock badges |
| Automation → Spotify | sketchybar (prompted on first use) | play/pause |
After brew upgrade sketchybar the Cellar path changes and the Accessibility
grant is lost (badges turn grey): remove the old entry and run --tags permissions.
The playbook cannot automate these:
| Step | Why |
|---|---|
| Raycast hotkey (Settings → General) | stored in an encrypted internal database |
| Register the signing key on GitHub | account-side, needs your session |
Fill in ~/.gitconfig-work |
the repo ships a redacted placeholder; force: false keeps your local value |
| Approve system extensions | Logitech and similar installers need Privacy & Security approval |
| AeroSpace Accessibility permission | TCC prompt, needs user approval |
| Keep a single macOS Desktop | AeroSpace only manages windows on the current Space |
Firefox full-screen-api.macos-native-full-screen = false |
about:config, per profile; avoids fullscreen video creating its own Space |
Lint and validate before pushing — CI runs the same three checks:
yamllint .
ansible-lint
ansible-playbook --syntax-check daily-driver.yml.ansible-lint carries a skip_list of rules that are deliberately deferred
rather than disabled; each entry is commented with what it covers.