A Guided Tour of My Dotfiles
My dotfiles
Over the years, I've refined my development environment into a lean, reproducible system that I can spin up on any machine in minutes. When I get a new machine, I didn't spend hours installing applications by hand or copy-pasting terminal configurations. I just cloned a single repository, ran ./install, and let automation do the heavy lifting.
If you're looking to streamline your workspace, here is an inside look at how I manage my dotfiles using Dotbot, Homebrew Bundle, and Git Town.
The Directory Structure
My dotfiles live in a GitHub repository (jianyuan/dotfiles).
Here is what the directory structure looks like:
At a glance, you can spot the three pillars of this setup:
- Dependency Management:
Brewfilehandles apps, binaries, and fonts. - Configuration Symlinking:
install.conf.yamland theinstallscript manage Dotbot. - Tool-Specific Configs: Dedicated files for Git and
direnv.
1. Automation with Dotbot
Instead of writing a fragile, custom bash script to symlink files into my home directory, I rely on Dotbot. It's a modular configuration manager designed to bootstrap environments effortlessly using a simple YAML configuration file.
The Execution Script: install
The install script pulls down Dotbot as a Git submodule, initializes it, and hands off the execution to the config file:
#!/usr/bin/env bash
set -euo pipefail
CONFIG="install.conf.yaml"
DOTBOT_DIR="dotbot"
DOTBOT_BIN="bin/dotbot"
BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "${BASEDIR}"
git -C "${DOTBOT_DIR}" submodule sync --quiet --recursive
git submodule update --init --recursive "${DOTBOT_DIR}"
"${BASEDIR}/${DOTBOT_DIR}/${DOTBOT_BIN}" -d "${BASEDIR}" -c "${CONFIG}" "${@}"The Blueprint: install.conf.yaml
Dotbot reads this declarative YAML file to know exactly what to do. It establishes global link defaults, handles symlinking, cleans up dead links, ensures my projects directory exists, and kicks off the package installer:
- defaults:
link:
create: true
relink: true
- link:
~/.gitconfig:
~/.gitignore_global:
~/.config/direnv: direnv
- clean: ['~']
- create:
- ~/code
- shell:
- command: brew bundle install
stdout: trueDefaults
My defaults section sets the global preferences that apply to all of my link operations, unless I override them in a specific link configuration:
- create: Automatically creates any parent directories needed to establish a symlink if they don't exist yet.
- relink: Automatically removes existing symlinks if I'm creating a new symlink with the same target name, which prevents conflicts.
Clean
In the clean section, I specify the paths where I want to clear out dead symlinks (links pointing to locations that no longer exist). In this configuration, I target my home directory (~) to ensure any dangling symlinks are swept away.
Link
The link section is where I map out which files or directories from my dotfiles repository should be symlinked to my home directory. Each entry maps a source file in my repository to its target path in my home directory.
Create
I use the create section to specify directories I need the system to spin up if they don't already exist.
Here, I'm explicitly creating the ~/code directory to ensure I always have a consistent location for my projects.
Shell
The shell section contains the commands I want to run after all my link operations and cleanups are finished.
Each command executes in the shell, and I use the stdout option to control whether I see the command's output.
In this setup, I run brew bundle install to automatically install all the dependencies I've defined in my Brewfile.
2. Declarative Packages with Homebrew (Brewfile)
Instead of maintaining a long list of brew install commands, I use a Brewfile.
Running brew bundle install natively parses this file and installs everything from CLI utilities to GUI applications and specialized programming fonts.
My setup spans a few core buckets:
- Core Utilities: Tools like
fzf(fuzzy finding),ripgrep(rg),fd,tmux, andneovimkeep the terminal fast and searchable. I usemisefor managing dev environments and runtime engines, andzoxideas a smartercd. - The Git Workflow: Alongside standard
gitand the GitHub CLI (gh), I usetigfor text-mode interface browsing andgit-townfor high-level branching strategies. - Virtualization:
colimakeeps my Docker containers lightweight without needing the full Docker Desktop GUI app, alongside standarddockertools andutmfor running VMs. - Mac App Casks: Essential workflow apps like
raycast,bitwarden,tailscale-app, andnordvpninstall in the background automatically. - Typography: Pure coding comfort with
Cascadia Code Nerd Fonts(font-cascadia-code-nf), which natively support dev icons and programming ligatures.
3. Advanced Git Superpowers
My .gitconfig is optimized for high-velocity software engineering.
It enforces modern defaults, hooks up automatic GitHub authentication, and embeds an entire suite of aliases dedicated to Git Town.
[user]
name = Your Name
email = your-email@example.com
[core]
excludesfile = ~/.gitignore_global
[commit]
gpgsign = true
[pull]
rebase = true
[push]
autoSetupRemote = true
[credential "https://github.com"]
helper =
helper = !/opt/homebrew/bin/gh auth git-credentialGit Town Integration
If you haven't used Git Town, it automates collaborative Git workflows (like maintaining feature branches, stacking changes, and staying synced with upstream origins). I map them out using dense aliases so that collaborative branching is a breeze:
[alias]
append = town append
compress = town compress
contribute = town contribute
hack = town hack
sync = town sync
ship = town ship
# ... shortcuts for prepend, propose, rename, and observe
[git-town]
sync-feature-strategy = rebaseI back this up with a .gitignore_global that aggressively filters out compiled object binaries (.o, .so), heavy packages, local SQLite files, standard macOS trash (.DS_Store), and sensitive .env or .envrc files before they can ever accidentally be staged.
4. Seamless Context Switching with direnv
The last piece of the puzzle is direnv.
It hooks into your shell to automatically load or unload environment variables depending on your current directory.
I keep a minor tweak inside direnv/direnv.toml:
[global]
load_dotenv = trueSetting load_dotenv = true means that whenever I step into a project folder containing a local .env file, direnv automatically exports those values directly into my shell ecosystem without requiring manual sourcing.
Keeping It Maintained
A common pitfall with dotfiles is that they rot. Tools update, schemas change, and plugins break. To combat this, I added a simple renovate.json config:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"]
}By putting the repository on GitHub and turning on Renovate Bot, I get automated Pull Requests whenever dependencies or configuration schemas have updates. It keeps the repo evergreen with zero ongoing maintenance on my end.
Bootstrapping a New Machine in 2 Steps
Now, whenever I format my Mac or spin up a new machine, setting everything up looks exactly like this:
# Step 1: Clone the environment
git clone https://github.com/jianyuan/dotfiles.git ~/dotfiles
# Step 2: Install everything
cd ~/dotfiles
./installWithin a few minutes, Dotbot maps my system config files, Homebrew bundles my entire software stack, and my terminal is prepped and ready for action.
Feel free to dive into my full configurations over at jianyuan/dotfiles, fork it, or cherry-pick configs to supercharge your own development workflow!
My dotfiles