Overview

One repo, every machine

These dotfiles are the single source of truth for one developer's environment across five machines — a MacBook Pro, a Mac mini, a Raspberry Pi Ansible controller, a local server, and a Hetzner VPS. chezmoi renders the right configuration for each host from one templated repo, so a single apply brings any machine to a known state.

Relevant source files
5
Machines
127
Homebrew pkgs
10
Lifecycle scripts
8
Helper tools

System architecture

A single chezmoi apply — or dots update — brings any host to a known state: it pulls the source, renders this machine's slice, and writes it into place. The same round trip runs on all five machines.

One apply brings any host to a known state
You chezmoi GitHub machine apply / update git pull source + data render this host's slice write configs · run_ known state1 of 5 · 2 macOS · 3 Linux done Macs also push changes back (dotsync)Linux hosts pull only

Sources: README.md .chezmoidata.yaml install.sh

Where to start

Bringing up a clean Mac is one command — the guided installer authenticates to GitHub, then applies everything (see Installation):

new machine
$ bash <(curl -fsSL https://cdn.levine.io/install.sh)

After that, the two Macs keep each other in sync with one command — and it runs itself, on a LaunchAgent, at login and every ~4 hours:

dotsync
$ dotsync          # sync now: pull + apply, then capture + push
$ dotsync status   # what's in / out of sync
$ dotsync help     # everything else (adopt / harden / macos)

Change anything on either Mac and it converges — installs and removals.

The map

Get set up

Installation and the machines the repo targets.

Understand it

How chezmoi works and the lifecycle scripts that run on apply.

Scope of this wiki
These pages document architecture, not credentials. SSH host topology, RustDesk peer records, and the /etc/hosts payload are excluded — they're operational data, not documentation. The source repository is private for the same reason (see Secrets & safety).

Setup · 01

Installation

Two paths, by machine type: a guided installer for Macs, and Ansible for the Linux fleet. Because the repo is private, a bare machine must authenticate to GitHub before it can clone — the installer handles that for you.

Relevant source files
The guided installer, end to end
You install.sh chezmoi GitHub bash <(curl … install.sh) install Homebrew · chezmoi · gh gh auth login (browser) authenticated chezmoi init --apply clone private repo source state render dotfiles → $HOME bootstrap · run_ scripts 10 → 90 machine ready

Sources: install.sh155–168 run_once_before_10-… …_20-brew-bundle

Mac — guided installer

One command on a clean Mac. It installs Homebrew, installs chezmoi and gh, signs you into GitHub through the browser, then applies the dotfiles:

bash
$ bash <(curl -fsSL https://cdn.levine.io/install.sh)

cdn.levine.io/install.sh is a Cloudflare redirect to a public gist (no secrets), fetched anonymously; the installer then authenticates to reach the private dotfiles. Add -y to accept every prompt automatically. The command it ultimately runs is chezmoi init --apply davelevine install.sh155–168.

Once dotfiles apply, chezmoi runs the bootstrap automatically:

  1. Install Homebrew if missing, before any files land run_once_before_10-…
  2. Apply the dotfiles, including ~/.Brewfile.
  3. Run brew bundle --global, installing every formula, cask, and Mac App Store app …_20-brew-bundle

Mac — already authenticated

If GitHub credentials are already present (gh auth login done, or a PAT in the keychain), skip the installer and go straight to chezmoi:

bash
$ sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply davelevine

Post-install — secrets & App Store

Templated files reference groqApiKey / ntfyAccessToken; until you create a local chezmoi.toml they render as empty strings. To fill them in (values from Bitwarden Secrets Manager) and re-apply:

bash
$ mkdir -p ~/.config/chezmoi
$ cat > ~/.config/chezmoi/chezmoi.toml << 'EOF'
[data]
    groqApiKey = "from-bitwarden"
    ntfyAccessToken = "from-bitwarden"
EOF
$ chezmoi apply

Sign into the App Store first
Mac App Store apps (mas entries) require being signed in, or brew bundle skips them with a warning. Sign in, then run chezmoi apply again.

Linux — Ansible & fallback

Managed Linux hosts are provisioned by an Ansible dotfiles role, which installs chezmoi, templates secrets from Bitwarden, and runs chezmoi apply:

ansible-control
$ ./ansible-bw.sh playbooks/site.yml --tags dotfiles       # all hosts
$ ./ansible-bw.sh playbooks/control-node.yml --tags dotfiles

For one-off boxes outside the fleet, the same guided installer runs on Linux too — it installs chezmoi via get.chezmoi.io, authenticates with gh or a Personal Access Token over HTTPS, then applies. Unlike the Ansible role it does not template secrets, so they stay empty until you create chezmoi.toml install.sh105–153.

Setup · 02

Machines

Five hosts, one repo, each bootstrapped a little differently. The two Macs share the same config — the darwin gate covers both — and sync to each other through GitHub. The Linux hosts each render their own slice.

Relevant source files
HostnameMachineAdminBootstrap
daves-mbpMacBook Pro (workstation)davechezmoi init --apply
daves-mac-miniMac mini (workstation)davechezmoi init --apply
ansibleansible-control (Raspberry Pi)daveAnsible role (SSH)
xenlabLocal serverdaveAnsible role (HTTPS + PAT)
ubuntu-8gb-nbg1-1Hetzner VPSunifiadminAnsible role (HTTPS + PAT)

How files are scoped

.chezmoiignore.tmpl evaluates on each machine and lists the paths chezmoi should skip there. Because it's a template gated on {{ .chezmoi.hostname }} and {{ .chezmoi.os }}, one repo yields a full Mac config and a lean headless-server config with no branching .chezmoiignore.tmpl5–34.

What each host receives

Both Macs

Everything: shell, Homebrew, macOS settings, LaunchAgents, apps, and the full sync tooling. They auto-sync to each other.

Linux hosts

Shell config only — .zshrc, .gitconfig, plus a few host-specific extras. No Mac setup scripts run, and updates are manual (chezmoi update).

Foundations · 03

How chezmoi works

chezmoi keeps one source repo and renders it into your home directory, selecting and templating files per host. There's no separate manifest for where files go or what permissions they carry — that's encoded in each file's name. Throughout the repo, dots is the shell alias for chezmoi.

Relevant source files
One apply, one file — the sequence
You chezmoi host data $HOME dots apply for each source file → ① decode name · dot_ private_ +x read values .yaml + .toml ② render Go template · per host write ~/.gitconfig · 0644 ③ run_ scripts interleaved → applied

Sources: dot_gitconfig.tmpl .chezmoidata.yaml private_dot_ssh/config

Source-state naming

Learn the prefixes and you can read the repository directly — the filename tells you exactly what chezmoi will produce. So private_dot_ssh/config deploys as an owner-only ~/.ssh/config.

Prefix / suffixEffect on applyExample
dot_Becomes a leading .dot_zshrc.tmpl
private_Restricts permissions to the owner (0600) — not encryptionprivate_dot_ssh/config
executable_Sets the executable bitexecutable_dotsync
create_Creates if absent; never overwrites later editscreate_config.yaml
run_once_ / run_onchange_Script run during apply — once, or when its content changes…_20-brew-bundle
.tmplRendered as a Go template, per hostdot_gitconfig.tmpl

Templating

Files ending in .tmpl render through Go's template engine at apply time. This repo uses it for three jobs: per-host differences (a Git safe.directory that exists only on xenlab), per-OS branches (LFS filters only on macOS), and secret injection (an API key read from local config). Values come from two layers — non-sensitive defaults in .chezmoidata.yaml, and machine-specific overrides in an untracked ~/.config/chezmoi/chezmoi.toml dot_gitconfig.tmpl.

The apply lifecycle

During chezmoi apply, run_ scripts execute in filename order, interleaved with file changes. A run_once_ script runs a single time per machine; a run_onchange_ script re-runs whenever its rendered content changes — triggered here by embedding a sha256 of the file it depends on, so editing that file changes the script and forces a re-run. _before_/_after_ place a script relative to file application; the number orders scripts within a phase. See Lifecycle scripts for the sequence.

private_ is not encryption
private_ only restricts file permissions to the owner. The contents are committed to Git in plain text. Confidential material is kept out of the tree entirely (templated from local config), not protected by this prefix — see Secrets & safety.

Foundations · 04

Lifecycle scripts

chezmoi runs shell scripts at defined points during apply. This repo has ten, numbered to fix their order. One runs once per machine to bootstrap Homebrew before any files land; the rest are on-change hooks that re-run only when their input changes, so a no-op apply stays silent. All are gated to macOS.

Relevant source files
Each script, delegated to its subsystem
chezmoi Homebrew System GUI apps BEFORE FILE APPLY · run once 10 · install Homebrew FILE APPLY · write dotfiles → $HOME AFTER FILE APPLY · on-change 20 · brew bundle 30 · load LaunchAgent 40 · VS Code extensions 50 · macOS defaults 60 · iStat Menus 70 · RustDesk server 80 · /etc/hosts block 85 · Arc DNS policy 90 · ForkLift theme on-change · re-runs only when its hashed input changes

Sources: run_once_before_10-… …_20-brew-bundle …_50-macos-defaults

The sequence

10

Install Homebrewonce · before

Runs the official non-interactive installer if brew is missing — before any files are applied.

run_once_before_10-install-homebrew.sh.tmpl
20

Brew bundleon-change

Runs brew bundle install --global (one retry), then uninstalls leaves dropped from the Brewfile — capped at 8 removals for safety — and verifies with brew bundle check. Hashes dot_Brewfile.

run_onchange_after_20-brew-bundle.sh.tmpl
30

Load LaunchAgenton-change

Re-bootstraps the io.levine.dotsync agent via launchctl when its plist changes, and retires the old Brewfile-only agent.

run_onchange_after_30-load-launchagent.sh.tmpl
40

VS Code extensionson-change

Installs the pinned extension list (Claude Code, Dracula, Prettier, Remote-SSH, Tailscale…) for any not already present. Triggered by editing the inline list.

run_onchange_after_40-vscode-extensions.sh.tmpl
50

macOS defaultson-change

Runs macos-defaults.sh to apply System Settings and keyboard shortcuts. Runs after 20 so Dock app tiles already exist. Hashes the script + hotkeys plist.

run_onchange_after_50-macos-defaults.sh.tmpl
60

iStat Menuson-change

defaults import restores the iStat Menus license, sensors, and menu-bar layout from four committed plist snapshots, then relaunches it.

run_onchange_after_60-istatmenus.sh.tmpl
70

RustDesk serveron-change

Upserts the self-hosted relay/rendezvous server and public key into RustDesk2.toml — network keys only, device identity untouched. Rendered from .chezmoidata.yaml.

run_onchange_after_70-rustdesk.sh.tmpl
80

/etc/hosts blockon-change

Maintains a single marked block in /etc/hosts via sudo, leaving Apple's defaults and Docker's entries untouched.

run_onchange_after_80-etc-hosts.sh.tmpl
85

Arc DNS policyon-change

Writes a Chromium enterprise policy plist to /Library/Managed Preferences/ (via sudo) that turns off Arc's built-in async DNS client, so it defers to the system resolver and rides through Tailscale MagicDNS changes cleanly instead of desyncing.

run_onchange_after_85-arc-dns-policy.sh.tmpl
90

ForkLift themeon-change

Merges a Dracula color theme into ForkLift's preferences (via an embedded Python step), preserving any other themes. Triggered by editing the inline colors.

run_onchange_after_90-forklift-dracula-theme.sh.tmpl

Everyday · 05

Day-to-day usage

Three things you'll actually do: change a file, push it, pull it elsewhere. On the Macs, dotsync automates all of this — these commands are for when you want something to happen right now, and they're the whole workflow on the Linux hosts.

Relevant source files

Making changes

Edit → commit → push. Always use dots edit — it opens the file's source (templates included), so the change sticks:

zsh
$ dots edit ~/.zshrc       # edits the source; applies locally on save
$ dots cd                # enter the source repo
$ git add -A
$ git commit -m "update zshrc"
$ git push
$ exit

Hand-edits don't stick
Editing ~/.zshrc directly is lost on the next apply — chezmoi rebuilds it from the template. dots re-add can't recover it either; it skips templated files. It only helps for plain, non-templated files like VS Code's settings.json.

Receiving changes

On another machine, pull and apply in one step — equivalent to git pull + chezmoi apply:

zsh
$ dots update

Other useful commands

zsh
$ dots diff              # preview what would change before applying
$ dots apply             # apply source state without pulling
$ dots re-add            # save a hand-edited plain file back to source

Everyday · 06

Sync workflow

dotsync is the one command to remember. On its own it runs the full round trip — pull + apply, then capture this Mac's drift and push — so you never string the low-level verbs together by hand. It self-locks, and a LaunchAgent runs it at login and every ~4 hours.

Relevant source files

Commands

dotsyncThe everyday driver. Pull + apply, then capture drift (Brewfile, keyboard shortcuts, RustDesk peers) and push. executable_dotsync59–69
dotsync statusRead-only dashboard: ahead/behind GitHub, pending applies, Brewfile drift. Changes nothing.
dotsync adopt [--force]Bulk-adopt hand-installed apps into Homebrew (wraps adopt-apps.sh).
dotsync hardenApply macOS security / privacy settings (wraps macos-security.sh).
dotsync macosRe-apply System Settings via defaults (wraps macos-defaults.sh).
dotsync helpList all subcommands.

What a run does

  1. Lock — an atomic mkdir lock means a scheduled run and a manual one can't collide; a second run just exits.
  2. Self-heal the branch — if the source is on a stale feature branch already merged into main, switch back to main and delete it.
  3. Pull + applychezmoi update (this also fires any run_ scripts whose inputs changed).
  4. Guard — if not on main, stop before capturing, so mid-work branches are never disturbed.
  5. Capture & pushbrew-bundle-sync.sh for packages, a defaults export for keyboard shortcuts, and rustdesk-peers-capture.sh for peers; commit and push only if something changed.
A dotsync run, step by step
LaunchAgent dotsync chezmoi GitHub trigger · login / ~4h ① lock (mkdir) · ② branch → main chezmoi update git pull new commits apply files + run_ scripts applied ③ guard — abort if not on main ④ capture drift — brew-bundle-sync,defaults export, rustdesk-peers commit + push (only if changed)

Sources: executable_dotsync59–69 brew-bundle-sync.sh

Two directions

GitHub is the only hub — there's no direct Mac-to-Mac sync. Each machine pushes up and pulls down. The chezmoi verbs dotsync is built from, when you want a single direction:

Topology — GitHub is the only hub
daves-mbp macOS GitHub private origin daves-mac-mini macOS push pull · apply push pull · apply LINUX HOSTS PULL ONLY · DOTS UPDATE

Sources: executable_dotsync io.levine.dotsync.plist.tmpl

CommandDirectionWhat it does
dots applyrepo → homeRender source into $HOME and run changed run_ scripts. No network.
dots updateGitHub → homegit pull then apply. Inbound only — never pushes.
dots re-addhome → sourceCopy a hand-edited plain file back into the source (skips templates).

Automatic syncing

A launchd agent, io.levine.dotsync (installed by lifecycle script 30), runs dotsync at login and every four hours (StartInterval 14400) at low I/O priority, logging to ~/.local/state/dotsync.log io.levine.dotsync.plist.tmpl14–17.

Removals propagate
Remove a package on one Mac and the next apply on the other uninstalls it there too (leaves-only, capped at 8) — so the Macs converge on removals, not just the union of installs.

Everyday · 07

Packages

~/.Brewfile is a Homebrew Bundle manifest naming every formula, cask, and Mac App Store title the Macs carry — 127 entries across 2 taps. It's generated, not written: the sync dumps current state into it and pushes, so both Macs reproduce from the manifest alone.

Relevant source files

Inventory

The adopt-and-dump loop

Each sync refreshes the Brewfile in two steps. First, adopt: auto-adopt-apps.sh scans /Applications for cask-available apps that aren't managed yet and match the cask version exactly, then adopts each with brew install --cask --adopt (never --force). Each app is copied aside first and restored if a failed adopt would remove it. Then dump: brew bundle dump snapshots Homebrew state and chezmoi re-add folds it back into the repo, committing only if the Brewfile actually changed.

Adopting apps by hand

To bring a newly hand-installed app under management, run the helper in a real Terminal:

Ghostty
$ ~/.local/bin/adopt-apps.sh           # adopt apps matching the cask version
$ ~/.local/bin/adopt-apps.sh --force   # upgrade-and-adopt stragglers

Use a real Terminal
--force does a full remove-then-reinstall; without Full Disk Access an error mid-way can leave an app deleted. Quit the apps first, and never run with sudo — Homebrew refuses to run as root. Logs: ~/.local/state/auto-adopt.log.

Manual installs (no cask)

These have no Homebrew cask and must be reinstalled from source on a fresh machine: Aerial Companion, Caldera Amp, CompressX, DeskRest, Family Tree Maker 2019, Feishin-plex, Folder Tidy, Greendot, HiFidelity, Kigo Movie One, SD Card Formatter.

System

System

Deeper configuration: macOS System Settings and hardening, the shell environment and prompt, and the helper scripts that tie it all together.

System · 08

macOS system

macOS state is reproduced as code — System Settings via defaults, a conservative security pass, and a few app snapshots. What restores automatically and what stays manual is spelled out below.

Relevant source files

System Settings

macos-defaults.sh writes a hand-curated snapshot of preferences that differ from stock — Dark mode, natural scrolling off, always-show scroll bars, 24-hour clock, Finder in list view with folders first, a left-side auto-hiding Dock with a fixed app order and a screen-saver hot corner, trackpad tap-to-click, custom pointer speed, and ForkLift as the default file viewer. It's applied automatically on apply via lifecycle script 50; re-assert it by hand with:

zsh
$ dotsync macos

Security hardening

macos-security.sh applies a conservative, reversible set of privacy settings: require password immediately after sleep, the application firewall on (stealth mode deliberately off so LAN tools keep working), the guest account disabled, and Touch ID for sudo via a pam_tid drop-in that survives OS updates. It deliberately leaves all remote access untouched — SSH, Screen Sharing, RustDesk, Tailscale — to avoid locking yourself out. FileVault and a firmware password are left for you to enable manually.

zsh
$ dotsync harden   # prompts for sudo (firewall + guest account)

What restores, what doesn't

Casks and Mac App Store apps come back from the Brewfile automatically. GUI-app preference plists are mostly not managed — they're binary, churn on every launch, and don't restore reliably through cfprefsd caching. The exception is iStat Menus, whose license and menu-bar layout are snapshotted and re-imported on apply (lifecycle script 60). Keyboard shortcuts are captured too, exported as symbolichotkeys during each dotsync.

System · 09

Linux hosts

Three Linux machines — ansible (a Raspberry Pi controller), xenlab (a local server), and ubuntu-8gb-nbg1-1 (a Hetzner VPS) — share the same repo as the Macs, each rendering its own slice. They're provisioned by Ansible, not the guided installer, and receive shell configuration only — none of the macOS setup scripts run.

Relevant source files
How a Linux host is provisioned — Ansible
You ansible-bw.sh Bitwarden Linux host ansible-bw.sh --tags dotfiles fetch secrets (SM) groqApiKey · ntfyToken SSH · run dotfiles role install chezmoi · init repotemplate chezmoi.toml chezmoi apply → shell config host converged

Sources: dot_zshrc.tmpl dot_bashrc

The three hosts

All three run the Ansible dotfiles role; they differ only in role and how the deploy authenticates to GitHub.

HostRoleDeploy auth
ansibleansible-control · Raspberry PiAnsible role over SSH
xenlabLocal serverAnsible role · HTTPS + PAT
ubuntu-8gb-nbg1-1Hetzner VPS (unifiadmin)Ansible role · HTTPS + PAT

Provisioned by Ansible

The Linux fleet is set up from an Ansible control node, not the Mac installer. The dotfiles role installs chezmoi, initializes the repo, templates secrets where needed, and runs chezmoi apply — this is the source of truth for the managed hosts:

control node
$ ./ansible-bw.sh playbooks/site.yml --tags dotfiles   # all hosts
$ ./ansible-bw.sh playbooks/control-node.yml --tags dotfiles   # just ansible-control

Secrets fill in automatically
On the Macs you write chezmoi.toml by hand. On the Linux hosts the Ansible role templates it from Bitwarden Secrets Manager during the deploy — groqApiKey and ntfyAccessToken land without manual steps (see Secrets & safety).

What each host gets

Linux hosts render shell configuration only — the same source files as the Macs, minus everything macOS-specific. The ten lifecycle scripts are all gated to macOS, so none run here.

Shared with the Macs

.zshrc and .gitconfig, rendered from the same templates against each host's own data.

Host-specific extras

On ansible-control, .bashrc adds NVM and an ansible-playbook locale alias dot_bashrc

Not applied

No Homebrew, no brew bundle, no macOS defaults, LaunchAgent, iStat, RustDesk, or theme scripts.

Daily workflow

Nothing is automated the way dotsync drives the Macs — there's no LaunchAgent. A host changes only when you run the commands by hand:

chezmoi updatePull the latest from GitHub and apply it. Run it on the host to get changes made elsewhere.
dots edit ~/.zshrcEdit the master template, then commit + push. Editing the rendered file directly is lost on the next apply.

Standalone fallback

The guided installer also runs on Linux for one-off boxes outside the Ansible fleet — it installs chezmoi via get.chezmoi.io, ensures git, and authenticates to GitHub (gh if present, otherwise a PAT over HTTPS). Use it only when Ansible doesn't manage the machine.

No secrets on the fallback path
Unlike the Ansible role, the standalone installer does not template secrets from Bitwarden — groqApiKey and ntfyAccessToken stay empty until you fill them in locally.

System · 10

Shell & configs

The actual environment the dotfiles build — shell, prompt, Git, terminal, and window management. The Dracula Pro palette this wiki wears is the same one running in Ghostty, ncspot, and the shell prompt.

Relevant source files

Zsh (macOS)

Cached completions, EDITOR=nano, and zsh-autosuggestions + zsh-syntax-highlighting from Homebrew. Notable aliases dot_zshrc.tmpl15–37:

AliasExpands to
dotschezmoi — the primary management command everywhere
lsls -la
lsteza --tree
infomacchina (system info)
jdevA dev server launch with secrets injected from templates
snow / trainsnowmachine / sl | lolcat — for fun

Zsh (Linux hosts)

Oh My Zsh with the robbyrussell theme. Per-host blocks add: on ansible, an ssh-agent + key load, NVM with .nvmrc auto-switching, Bitwarden secrets, and cd ~/homelab-iac on start; on xenlab, smartctl disk-health aliases, an ollama-in-Docker alias, and pnpm on PATH.

Prompt (Oh My Posh "craver")

A two-line boxed prompt in Dracula Pro colors: OS icon, battery, time, an agnoster_full path, Git status with stash/upstream, .NET version, exit status, and command execution time (red past 1 s). Initialized from ~/Documents/craver.omp.json craver.omp.json.

Git & tools

Git identity Dave <dave@davelevine.io>, pull.rebase = true, LFS on macOS, and per-host credential.helper / safe.directory branches. Other configs: Ghostty (the dracula-pro theme), ncspot (Dracula theme), gh (git_protocol: https, alias co → pr checkout), code-server (bound to 0.0.0.0:8080, auth deferred to the network), and AWS CLI profiles (no secrets).

Window management (Hammerspoon)

An Amethyst-style tiling manager with four layouts — 3column (default), tall, tall-right, fullscreen — plus a browser/utility float allowlist. Key bindings: ⇧⌘Space cycles layouts, ⌥⇧A/S/D/F jump to a layout, ⌥⇧T toggles float, ⌥⇧R retiles, and ⌥⇧J/K move focus. A separate menu-bar speed test (⌃⌥⌘S) runs speedtest-go with a live gauge dot_hammerspoon/init.lua604–648.

System · 11

Scripts & tooling

The managed helpers that live in ~/.local/bin. Some are the machinery behind dotsync; others are standalone tools you run by hand. All target macOS (several are written for system bash 3.2).

Relevant source files

Sync machinery

dotsync

Orchestrates the full round trip: pull + apply, then capture drift and push. Self-locking.

run by · you + LaunchAgentexecutable_dotsync

brew-bundle-sync.sh

Dumps Homebrew → Brewfile in deterministic, sectioned form; commits and pushes with a rebase retry if changed.

run by · dotsyncbrew-bundle-sync.sh

auto-adopt-apps.sh

Version-exact adopts new cask apps with a backup/restore safety net so nothing is lost.

run by · brew-bundle-sync.shauto-adopt-apps.sh

rustdesk-peers-capture.sh

Snapshots RustDesk peers into the source — sanitised, password-free, and only when changed.

run by · dotsyncrustdesk-peers-capture.sh

Standalone tools

adopt-apps.sh

One-off bulk adoption of a curated cask list; --force upgrades stragglers. dotsync adopt.

run by · youadopt-apps.sh

macos-defaults.sh

Applies customized System Settings via defaults. dotsync macos, and auto on apply.

run by · you + applymacos-defaults.sh

macos-security.sh

Conservative security/privacy hardening; leaves remote access alone. dotsync harden.

run by · youmacos-security.sh

wifi-toggle.sh

Turns Wi-Fi off when Ethernet is active and back on when it drops, via a launchd agent watching network config.

run by · launchdwifi-toggle.sh

State & logs
Lock at ~/.local/state/dotsync.lock; sync log ~/.local/state/dotsync.log; adopt log ~/.local/state/auto-adopt.log; cask catalog cache ~/.cache/homebrew-cask-catalog.json (refreshed weekly).

Recovery · 12

Disaster recovery

The installer plus chezmoi apply restore every dotfile, package, cask, and App Store app. What's left is the manual layer a fresh Mac still needs — ordered by what unblocks you first.

Relevant source files
Rebuilding a lost Mac — automated, then manual
You Bitwarden chezmoi GitHub run install.sh (clean Mac) installs Homebrew · chezmoi · gh gh auth (browser) authenticated ③ sign in to App Store first fetch secrets + keys chezmoi.toml · ~/.ssh keys ① secrets · ② SSH keys restored chezmoi apply clone private repo source state restore dotfiles · brew bundle(pkgs · casks · mas) · macos-defaults ④ environment restored

Sources: install.sh …_20-brew-bundle macos-security.sh

The installer and chezmoi apply handle the top of this sequence automatically; the checklist below is the manual layer that remains, ordered by what unblocks you first.

The checklist

  1. Secrets — recreate chezmoi.toml with the groqApiKey / ntfyAccessToken values from Bitwarden, then chezmoi apply.
  2. SSH keys — restore ~/.ssh/ansible, id_rsa_mac, and id_rsa_hetzner from backup or Bitwarden. The keys are never committed; without them you can't reach other hosts or push to the repo.
  3. App Store / iCloud — sign in before brew bundle, or every mas app is skipped.
  4. System Settings — automatic on apply via macos-defaults.sh; run dotsync harden for the security pass (prompts for sudo).
  5. Licenses — re-enter for CleanMyMac, Bartender, ForkLift, TablePlus, Carbon Copy Cloner, HazeOver, Lunar, eM Client, Paragon NTFS, Xnapper. (iStat Menus is automatic.)
  6. Permissions — grant Accessibility, Screen Recording, Input Monitoring, etc. when each app first prompts.
  7. Manual apps — reinstall the no-cask apps listed under Packages.
  8. App settings — reconfigure GUI apps whose preferences aren't managed (Reeder, Lunar, HazeOver, Alcove…).

Connection data lives elsewhere
App connection lists (Windows App / RDP, the RustDesk address book, Arc) are secret-bearing and churny, so they stay out of the repo. The Carbon Copy Cloner backup keeps them instead — restore by browsing the backup in Finder and copying the paths back.

Recovery · 13

Secrets & safety

No secret value is committed to this repository. Templated files reference variables — an API key, a notification token — but their definitions in .chezmoidata.yaml are empty strings. Real values live only in an untracked local config, injected at apply time.

Relevant source files

How secrets are handled

A fresh machine applies cleanly with secret variables blank until you supply them. Values come from ~/.config/chezmoi/chezmoi.toml, whose [data] section overrides the empty defaults; on the Ansible-managed Linux hosts, that file is templated from Bitwarden Secrets Manager automatically .chezmoidata.yaml4–5.

LayerTracked in Git?Holds
.chezmoidata.yamlYesEmpty defaults & non-secret values
~/.config/chezmoi/chezmoi.tomlNo — local onlyReal secret values & host specifics
Rendered dotfilesN/A — generatedSecrets injected at apply time

What's intentionally omitted

Beyond secrets, some tracked files are operational data rather than documentation and are kept out of this wiki: the SSH client config (host topology), the RustDesk address book, and the reference entries in the /etc/hosts block. Even the RustDesk peers that are versioned are sanitised — rustdesk-peers-capture.sh strips passwords and volatile fields before they ever reach the repo. This is also why the repository itself stays private: publishing it would expose that map even though no credentials sit in it.

Verified clean
A scan of the full commit history — every blob across all 166 commits — found no API keys, tokens, or private-key material. The secret placeholders have been empty in every revision.