The Modular Blueprint

Why I Rebuilt My Shell from Scratch

For a long time, my terminal was a growing mess. Every time I need a new alias or a snippet for Git, I’d just add it at the bottom of my .zshrc. It worked, but it was brittle. If functionality broke, like a completion conflicted, I had to scroll through a hundreds of lines of code and try to find the culprit.

As I’ve progressed in my career and studies and started managing more complex environments, I realised that the shell isn’t just a place to type commands, it is my primary interface. It is the foundation for every other tool I use, from Neovim to git and Kubernetes. If that foundation is messy, my whole workflow feels slugish.

I decided to treat my shell configuration like a project: modular, documented, and clean. Here is how I structured my environment and how it’s changed the way I work.

The Goal: Intuitive Flow

I wanted an environment that was fast, provided instant context (like what Git branch I’m on, etc), and—most importantly, that was easy to extend and maintain. I moved away from heavy “all-in-one” frameworks and built a modular system that I could understand from the ground up. Also, I didn’t need bloated solutions that push swag in the shell (looking at you oh-my-zsh )

Step 1: Cleaning up the Home Directory

The first thing I did was get my configuration files out of my home root. By default, Zsh was littering my home directroy with hidden files. I wanted everything out of the way in one place: ~/.config/zsh.

To do this, I use a file called .zshenv in my home directory. It has one job: tell Zsh where to look for the rest of my logic.

# ~/.zshenv
export ZDOTDIR=$HOME/.config/zsh
ZDOTDIR MAGIC

In Zsh, ZDOTDIR is a built-in environment variable that defines the directory where the shell looks for its configuration files. By default, if this variable is not set, Zsh looks in your home directory ($HOME).

Now all my Zsh files like, my history, my aliases, and my plugins are all neatly tucked away in an XDG Base1 compliant folder. It makes backing up my “dotfiles” or moving to a new machine much simpler.

Step 2: The Logic Layer

Instead of just sourcing files and hoping they exist, I wrote a small helper function in a file I named zsh-functions. This allows me to load modules safely. If I’m testing a new config and I delete a file, my shell won’t throw ugly errors on startup.

# Inside zsh-functions
function zsh_add_file() {
    [ -f "$ZDOTDIR/$1" ] && source "$ZDOTDIR/$1"
}

I also built built my own lightweight plugin manager, a zsh_add_plugin function (more on that in another article). I don’t use a heavy plugin manager (like oh-my-zsh); I just declare a GitHub repository, and my shell clones it and sources it automatically. It’s lightweight and gives me total control.

Step 3: The Orchestrator (.zshrc)

With a foundation now set, my ~/.config/zsh/.zshrc becomes more of an orchestrator rather than a configuration store. It sets my preferences, like turning off that annoying terminal beep (unsetopt BEEP), and calls (sources) my modules in order.

I separate my configuration out into specific files for clarity:

  • zsh-exports: For my global paths and default apps (like setting Neovim as my editor).
  • zsh-aliases: For my shortcuts and safety checks (like making sure I’m asked before deleting a file with rm -i).
  • zsh-vim-mode: This is a big one for me—it lets me use Vim motions right on the command line.

Why This Works For Me

By separating these out, I’ve made my shell resilient. For example, I recently had an issue where my ls aliases were fighting with my tab-completion. Because I had a dedicated zsh-aliases file, I could isolate the problem, swap my aliases for functions, and fix it in seconds without touching my Vim bindings or my prompt theme.

Having a shell that I can customise and manage to meet my needs means I’m not fighting my tools; I’m adapting them to me and using them efficiently. I am enjoying using my shell and toolage as I can flow better through my tasks.


  1. Probably more then what you wanted to know about ZDOTDIR and XDG. Bootstrap zsh to XDG Standard ↩︎