Back to blog

A Guided Tour of My Dotfiles

·6 min read
#shell#productivity
jianyuan/dotfiles 1

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:

Brewfile
gitconfig
gitconfig_global
install
install.conf.yaml
renovate.json
direnv.toml

At a glance, you can spot the three pillars of this setup:

  1. Dependency Management: Brewfile handles apps, binaries, and fonts.
  2. Configuration Symlinking: install.conf.yaml and the install script manage Dotbot.
  3. 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:

install
#!/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:

install.conf.yaml
- defaults:
    link:
      create: true
      relink: true

- link:
    ~/.gitconfig:
    ~/.gitignore_global:
    ~/.config/direnv: direnv

- clean: ['~']

- create:
    - ~/code

- shell:
    - command: brew bundle install
      stdout: true

Defaults

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.

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, and neovim keep the terminal fast and searchable. I use mise for managing dev environments and runtime engines, and zoxide as a smarter cd.
  • The Git Workflow: Alongside standard git and the GitHub CLI (gh), I use tig for text-mode interface browsing and git-town for high-level branching strategies.
  • Virtualization: colima keeps my Docker containers lightweight without needing the full Docker Desktop GUI app, alongside standard docker tools and utm for running VMs.
  • Mac App Casks: Essential workflow apps like raycast, bitwarden, tailscale-app, and nordvpn install 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.

gitconfig
[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-credential

Git 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:

gitconfig
[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 = rebase

I 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:

direnv/direnv.toml
[global]
load_dotenv = true

Setting 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:

renovate.json
{
  "$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
./install

Within 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!

jianyuan/dotfiles 1

My dotfiles