
Automated macOS Dev Setup
6 min readOne of my first GitHub open source projects I created 10 years ago was my dotfiles
repository. Everything started as a place to store my configuration files as a backup and a way to showcase my setup with friends.
Since then, it has evolved into an ever-changing collection of scripts, tools, and configurations files I use to set up my development environment.
A few weeks ago, I decided to completely rewrite my dotfiles with a tool called chezmoi
and it has been a complete game changer! 🤯
Table of Contents
Where I'm coming from
For years, I managed my dotfiles with a handcrafted shell script system. Those scripts, created the dotfiles, prompted for user input and installed software through brew.
It worked well for a good while, but it had a few limitations that I was not happy with:
- Complex to maintain: As my setup changed, the scripts were getting more complex over time, and I was spending too much time maintaining them. Debugging failures wasn't always straightforward.
- Machine-to-machine differences: I use different machines (personal and work) each with different needs and configurations.
- Difficult to track changes: When I made updates to the repo, I had to remember to run the setup script on all machines often skipping certain steps to avoid overwriting local changes.
Where I'm at now
I finally decided to give chezmoi
a try after hearing about it many times and I'm so glad I did 😍. It has completely changed the way I manage my dotfiles.
With just a single command, I can setup a fresh machine into my complete personal environment, with all my dotfiles, applications, tools and system settings included.
curl -sfL https://raw.githubusercontent.com/carloscuesta/dotfiles/master/.setup.sh | bash
I know, it feels like magic! 🪄, but let me show you how it works 🤓. You can follow along using my dotfiles repository.
Dotfiles
Dotfiles store configuration for many tools and apps on the system, from the shell to the editor, Git SSH, you name it. Chezmoi works by creating a 1:1 mapping between the files in your repository and their destination on your system.
Files prefixed with a dot_
are treated as dotfiles. For example, a file named dot_zshrc
in the repo will be created as ~/.zshrc
on your machine. By default, all files in the repository are managed by chezmoi, unless explicitly ignored.
Toggle dotfiles tree 👈
.
├── dot_config
│ ├── ghostty
│ │ └── config
│ ├── git
│ │ ├── config.tmpl
│ │ └── ignore
│ ├── npm
│ │ └── config
│ └── starship
│ └── starship.toml
├── dot_dotfiles
│ ├── dot_aliases
│ ├── dot_exports
│ ├── dot_extra
│ └── dot_functions
├── dot_ssh
│ └── config.tmpl
└──dot_zshrc
Looking at the tree above, you can start to see how dotfiles will be structured once applied to the system. But there's more, did you notice .tmpl
files? 👀
Template files
The .tmpl
files are Go templates that allow you to generate dotfiles with dynamic content, like environment variables, user input or other system information.
For example, dot_config/git/config.tmpl
is a template that gets compiled into a proper Git config file based on predefined variables and user prompts:
mention this is based on prompts and variables (not sure if remove the reference to dot_ssh/config.tmp)
[user]
name = Carlos Cuesta
email = {{ .email }}
signingkey = {{ .chezmoi.homeDir }}/.ssh/{{ .gitSigningKeyName }}.pub
Once the dotfiles are applied, chezmoi
will replace the {{ .variables }}
with the actual values. You can define variables and prompts in the .chezmoi.<format>.tml
file.
{
"data": {
"email": {{ promptString "What is your email" | quote }},
"setSshKey": {{ promptBool "Do you want to set up an SSH key" }},
"gitSigningKeyName": "id_github"
}
}
The templating engine is very powerful and there's a lot you can do with it, including: conditionals, functions and more.
This lets you keep your dotfiles clean, portable, and tailored to each machine, without hardcoding sensitive data or environment-specific settings.
Hooks
Another key part of the setup is installing software, tools, and applying macOS system settings. This is where hooks come into play, they let you automate tasks, like running scripts at the different stages of the setup process.
Here's how I use them in my setup:
Apply macOS system defaults
I've been using macOS for more than 15 years now, and I’m very picky about how my system is configured. From keyboard layout to trackpad, finder and dock settings, there are a bunch of preferences I always change to match my workflow.
Instead of applying these manually every time, I use a run once hook to automate it all. It makes the experience of setting up a new mac easy and consistent, every single time.
{{- if eq .chezmoi.os "darwin" }}
echo "🍏 Setting macOS defaults"
defaults write ...
{{ end -}}
Install apps, tools and packages
I use Homebrew alongside an inline Brewfile to install all the apps, tools, and packages I need to provision my development environment.
Instead of installing them manually, I use a run on change hook that runs every time the Brewfile
changes.
{{- if eq .chezmoi.os "darwin" -}}
brew bundle --file=/dev/stdin <<EOF
brew "..."
cask "..."
EOF
{{ end -}}
Install shell packages
Last but not least another run on change hook to install zsh plugins and Ghostty themes, to make my terminal look like this:
Setup script
To glue everything together, I have a simple shell script that I can run via curl. This script is the entry point to my dotfiles and it's responsible for:
- Installing
xcode
command line tools. - Installing
brew
. - Installing
oh-my-zsh
. - Installing
chezmoi
. - Initializing dotfiles or update them if they're already set up.
Toggle setup script 👈
#!/bin/bash
set -eufo pipefail
echo "🚀 Setting up @carloscuesta dotfiles."
if xcode-select -p &> /dev/null; then
echo "✅ Xcode command line tools are already installed."
else
echo "🔧 Installing Xcode command line tools..."
xcode-select --install &> /dev/null
echo "✅ Xcode command line tools installed successfully."
fi
if which -s "brew"; then
echo "✅ Homebrew is already installed."
else
echo "🍺 Installing Homebrew"
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo "✅ Homebrew installed successfully."
fi
if [ -f ~/.oh-my-zsh/oh-my-zsh.sh ]; then
echo "✅ oh-my-zsh is already installed."
else
echo "💻 Installing oh-my-zsh"
yes | sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
echo "✅ oh-my-zsh installed successfully."
fi
if which -s "chezmoi"; then
echo "✅ Chezmoi is already installed."
else
echo "⚪️ Installing Chezmoi"
brew install chezmoi
fi
if [ -d "$HOME/.local/share/chezmoi/.git" ]; then
echo "ℹ️ Chezmoi already initialized, pulling latest changes..."
chezmoi update
echo "✅ Chezmoi updated"
else
chezmoi init carloscuesta
chezmoi apply
echo "✅ Chezmoi initialized"
fi
Conclusion
I'm very satisfied with how my new dotfiles management system turned out. It's way more simple, scalable and flexible than what I had before.
If you're on the fence about trying out chezmoi
I genuinely recommend giving it a go. Start small and build up from there.
Let me know if you end up using it, I’d love to see your setup! 😍
Enjoyed the article? 😍