Modernizing My Dotfiles
During the recent first heatwave of this summer, I fled into the air-conditioned company office on the weekend. (A nice 15 °C temperature difference)
The focus that day was on my dotfiles. I’ve been storing my settings in this repo for almost 10 years now, but a recent hardware defect[1] showed me how valuable it is to have an always-up-to-date version of them.
Goals #
For a couple of months I collected ideas in a Things 3 project which turned into my goals for this weekend:
- Clean up the folder structure and have a few top-level folders.
- Update the
Brewfilewith the tools I use and add a script to keep it up to date with brews I install outside theBrewfile. - Create a
dotfiles-privaterepo to store more sensitive configs (SSH configs) and hook it up to thedotfilesrepo. - Improve the first setup and update experience, so setting up a new MacBook is easy and painless.
- Start saving skills related to AI agents in the dotfiles repo.
- Long-term, figure out how to move more software settings and configs to my dotfiles (TablePlus connections for example).
Long-term, I would like to have all software settings version controlled. I know that nix-darwin would be the ideal solution for this, but that’s a rabbit hole I’m not ready to go into … yet.
Inspiration #
I took the dotfiles of Freek and Duncan as inspiration.
Freek uses a simple folder structure and stores software configs in either a config or home folder. He has a vast list of Claude skills in his repo, which I would like to test and explore in the future.
Duncan uses Dotbot – as I do – for creating the symlinks and has nice settings for apps I would like to copy over.
I decided to go even more minimal than Freek and only have a bin and config folder. bin would house all future scripts related to the dotfiles (setup, install, updates, Homebrew sync). config would contain configuration and settings, each in a dedicated folder per app.
Approach #
The project started by moving app setting after app setting into the config folder and updating the symlinks.
With the help of an AI agent I created shell scripts for first-setup, update and a brewfile-sync script. brewfile-sync uses brew leaves to compile a list of installed brews and appends them to Brewfile. I then review the changes and commit the Brewfile.
Regarding skills: I've copied over some of Freek's skills and hooked them up to my current agent. I will test them out and cull the ones I don't end up using.
That's basically it for the public dotfiles repo. It wasn't as complicated as I thought it would be. As so often in life, you just have to get the ball rolling and get things done.
Private Dotfiles #
Next on my list was the dotfiles-private repository. For years I’ve shuffled SSH configs, TablePlus configurations and other crucial app settings which I don’t want to make public from computer to computer.
For example for Alfred I use a GitHub repository to sync/backup my settings, but I rarely committed the changes and pushed them to GitHub.
My idea with this dotfiles-private repo is to move more sensitive settings into a single repository instead of scattering them across many. And to automate the update process as much as possible.
For now, I moved my Alfred settings, SSH configs and one shell function into this repo. The folder structure mimics the public dotfiles repo.
.
├── bin
│ └── install
├── config
│ ├── alfred/
│ ├── shell/
│ ├── ssh
│ │ ├── config
│ │ ├── configs/
│ │ └── README.md
├── install.conf.yml
└── README.md
In config/ssh/configs/ I store one SSH config per organization or project I’m a part of. This covers work, open-source projects and my home lab infrastructure.
Here’s for example my general-config-file.
# General SSH Configs
# Contains general configs that apply to all environments.
Host *.github.com
AddKeysToAgent yes
IdentityFile ~/.ssh/primary-ed25519
IdentitiesOnly yes
Host github.com
AddKeysToAgent yes
IdentityFile ~/.ssh/primary-ed25519
IdentitiesOnly yes
Host unnamed-laravel-forge-server
HostName 0.0.0.0
User forge
IdentityFile ~/.ssh/primary-ed25519
IdentitiesOnly yes
The ~/.ssh/primary-ed25519 key isn’t stored on the disk though. When you take a look at the config/ssh/config-file, you might spot why.
Host *
SetEnv TERM=xterm-256color
# Split configs managed in ~/.dotfiles-private/config/ssh/configs/
Include ~/.dotfiles-private/config/ssh/configs/*
# Fallback: 1Password SSH agent for anything not matched above
Host *
IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
I point an Include statement towards an expansion path and let the ssh-agent do its magic.
The SSH key itself is not stored on disk, but managed by my 1Password instance.
Instead of typing the password for my SSH key every time I need to use it, 1Password will trigger a modal window and ask for my fingerprint to unlock the key for this request or the session.
I know what you might think: "This guy entrusts 1Password with his private SSH key?!" I get the hesitation. My own trust has been dwindling lately — the price increases, pushing AI into the backend, and a general slide in product quality (that sidebar spacing gives me nightmares). I use AI agents daily myself, so it's not the AI that bothers me — it's justifying a price hike because you can now rename credentials with it.
So I plan to migrate away — for my personal account — later this year and switch back to regular encrypted SSH keys.
Back to dotfiles. The config/alfred folder now contains my Alfred.alfredpreference bundle and I will now regularly commit and push it to the remote repository.
Right now I have empty config/raycast and config/tableplus folders too. I would like to move their configuration and settings here as well, but didn’t start with the process yet. Keep an eye on your RSS feed for a blogpost about this, once I have found a good setup. (An “Alfred vs. Raycast” blog post has been on my fingertips for years now.)
Shell Speed Improvements #
After Freek shared this blog post by Mijndert, I noticed (again!) how slow the startup speed of my shell has become (0.5 to 0.8 seconds).
I suspected that nvm was the problem here, and a quick disable of the initialization script confirmed the suspicion.
I never deliberately used nvm, but Laravel Herd enabled it for me, when you let it manage Node.js versions for you.
Switched back to fnm, which doesn’t have any impact on shell startup speed whatsoever. (I wonder if this is because it’s written in Rust.)
With nvm gone, I got rid of Oh My Zsh too. I never really understood the use case of it and none of its plugins were mission critical for me. One less thing in the configuration.
Shiny New Toys #
New however are zsh-patina (shell syntax highlighter), atuin (shell history in sqlite) and direnv (auto-load and unload environment variables per directory).
Still early-days for me with atuin and direnv, but I like them already. Especially direnv is becoming handy, as I write more and more scripts related to infrastructure lately.
Another new tool I don’t want to live without is peon-ping. A shell script that hooks into your AI agent of choice and triggers audio cues whenever the agent needs your input, fails or completes a task.
As a Warcraft fan I obviously use it with the w3_peon and w3_peasant pack. Never gets old to hear “Ready to work?” or “Work, work.” in my headphones when the agent works on something for me.
What’s next #
It's been a week since the refactor and I'm quite happy with it. There's something satisfying about opening a clean, well-organized repo. My dotfiles finally feel less like a pile of accumulated hacks and more like something I'd actually enjoy setting up on a fresh machine.
As mentioned, the next bigger steps are adding settings for Raycast and TablePlus to the repo. I also would like to give zellij and AeroSpace another try.
After shutting down my work MacBook Pro with the M3 Pro processor it wouldn’t boot anymore. The entire logic board was fried. ↩︎