zdot - Modular Zsh Configuration Framework
June 23, 2026 ยท View on GitHub

zdot - Modular Zsh Configuration Framework
zdot is a hook-based, dependency-aware configuration system for Zsh that organizes your shell environment into modular, reusable components with automatic dependency resolution.
๐ Rendered documentation: docs.georgeharker.com/zdot ยท dev
- Why zdot?
- Quick Start
- A Realistic
.zshrc - Directory Structure
- Modules
- Contexts and Variants
- CLI
- Debugging
- Day-to-Day
- Documentation
Why zdot?
Traditional plugin managers load everything in a single pass. As your shell
config grows -- secrets from 1Password, lazy-loaded nvm, ssh via 1Password
agent, brew-installed tools -- you end up with implicit ordering dependencies
scattered across one big .zshrc. Miss one and things break silently.
zdot fixes this:
- Declare, don't order. Each module says what it provides (
brew-ready) and what it requires (bootstrap-ready). zdot topologically sorts the rest. - Modular by design. One directory per concern. Swap, override, or skip modules without touching anything else.
- Fast. Execution plans are cached. All startup code is compiled to
.zwcbytecode. Deferred loading viazsh-deferkeeps the prompt instant. - Plugin management built in. Clone, load, and compile plugins from GitHub, Oh-My-Zsh, or Prezto -- integrated into the same dependency graph.
- Context-aware. Different hooks for interactive vs non-interactive, login
vs non-login, and user-defined variants (e.g.
workvshome).
Quick Start
There are two supported ways to run zdot, and both are first-class:
- Standalone -- clone zdot, source it from your existing
.zshrc. Right choice if you already have a dotfiles manager (stow, chezmoi, yadm) or just want to try zdot. - With dotfiler -- zdot lives inside a dotfiler-managed dotfiles repo. Recommended for a full setup; see why below.
Option A: Standalone
# 1. Clone zdot into your XDG config
git clone https://github.com/georgeharker/zdot \
"${XDG_CONFIG_HOME:-$HOME/.config}/zdot"
# 2. Add to your ~/.zshrc (or ${XDG_CONFIG_HOME}/zsh/.zshrc):
source "${XDG_CONFIG_HOME:-$HOME/.config}/zdot/zdot.zsh"
zdot_load_module xdg
zdot_load_module bootstrap
zdot_load_module env
zdot_load_module history
zdot_load_module brew # macOS only; skipped if brew not found
zdot_load_module keybinds
zdot_load_module completions
zdot_init
# 3. Restart your shell
exec zsh
That's it. zdot_init resolves dependencies, builds the execution plan, runs
everything in the right order, and compiles to bytecode for next time.
Standalone zdot can keep itself updated too -- opt in with
zstyle ':zdot:update' mode prompt (see the
zstyle reference).
Option B: With dotfiler (recommended)
dotfiler is a dotfiles manager that keeps your config repo in sync across machines. Neither tool requires the other -- but together they cover what each leaves out:
- zdot organizes what's inside your
.zshrc; it deliberately does not manage the rc files themselves. Your.zshrc/.zshenv, the rest of~/.config, and your user modules still need to live somewhere versioned and reach every machine. dotfiler tracks them as symlinks into a single git repo -- edit~/.zshrcin place and the change is already staged for commit. - One update cycle for everything. zdot registers as a dotfiler update hook, so a single login-time check updates your dotfiles, your rc files, and zdot together.
- Pinned, reproducible versions. As a git submodule, the exact zdot version is recorded in your dotfiles history, and by default only advances on tagged releases.
- One-command machine bootstrap.
dotfiler setup --bootstraprestores rc files, symlinks, and zdot on a fresh machine.
The happy path, from nothing:
mkdir -p ~/.dotfiles && cd ~/.dotfiles && git init
git clone https://github.com/georgeharker/dotfiler .nounpack/dotfiler
git submodule add https://github.com/georgeharker/zdot .config/zdot
Then follow the dotfiler + zdot quickstart --
it walks through the .zshrc, hook registration, unpacking the symlink tree,
and bootstrapping new machines.
A Realistic .zshrc
Here's a more complete example showing modules, plugins, deferred loading, and variants:
source "${XDG_CONFIG_HOME}/zdot/zdot.zsh"
# โโ Configuration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
zstyle ':zdot:update' mode prompt
zstyle ':zdot:cache' enabled true
# โโ Modules โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
zdot_load_module xdg
zdot_load_module bootstrap
zdot_load_module env
zdot_load_module history
zdot_load_module brew
zdot_load_module secrets # 1Password secrets
zdot_load_module nodejs # nvm + node
zdot_load_module rust
zdot_load_module fzf
zdot_load_module keybinds
zdot_load_module plugins # third-party zsh plugins
zdot_load_module omz # Oh-My-Zsh bundle defaults
zdot_load_module shell-extras # git, eza, ssh plugins
zdot_load_module completions
zdot_load_module starship-prompt
zdot_load_module local_rc # source ~/.zshrc_local if it exists
# โโ Defer control โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Acknowledge these hooks will be force-deferred
zdot_allow_defer _nodejs_init nodejs-configured
zdot_allow_defer _completions_init
# โโ Go โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
zdot_init
Directory Structure
zdot/
โโโ zdot.zsh # Main entry point (source this)
โโโ core/ # Framework internals (do not modify)
โ โโโ hooks.zsh # Hook registration & dependency resolution
โ โโโ modules.zsh # Module search path & loading
โ โโโ plugins.zsh # Plugin manager (clone/load/defer)
โ โโโ init.zsh # zdot_init() orchestration
โ โโโ cache.zsh # Bytecode compilation & plan caching
โ โโโ ctx.zsh # Context & variant detection
โ โโโ logging.zsh # Logging functions
โ โโโ functions.zsh # Function autoloading
โ โโโ completions.zsh # Completion registration
โ โโโ utils.zsh # Platform detection & utilities
โ โโโ compinit.zsh # Shared compinit machinery
โ โโโ update.zsh # Self-update integration
โ โโโ functions/ # Autoloaded CLI functions
โ โโโ plugin-bundles/ # OMZ & Prezto bundle handlers
โโโ modules/ # Built-in modules
โ โโโ xdg/
โ โโโ brew/
โ โโโ fzf/
โ โโโ ...
โโโ docs/ # Documentation
โโโ scripts/ # Benchmarking & profiling utilities
User modules live outside this tree (see Modules).
Modules
A module is a directory containing a single .zsh file of the same name. When
loaded, it registers one or more hooks that declare what they provide and
require. zdot resolves the dependency graph and executes hooks in the correct
order.
zdot ships 30+ built-in modules covering the common concerns:
| Module | Description |
|---|---|
xdg | XDG Base Directory setup |
env | Core environment variables |
history | Zsh history (XDG location, per-directory history) |
brew / apt | Package-manager PATH and tool verification |
secrets | 1Password secrets management |
nodejs | Node.js / nvm with lazy loading |
fzf | Fuzzy finder + integrations |
plugins | Third-party zsh plugins |
completions | Completion file generation + compinit |
starship-prompt | Starship prompt (also: omp-prompt, omz-prompt) |
| ... | Full catalog |
The simplest module is three lines:
# ~/.config/zdot-modules/mymodule/mymodule.zsh
_mymodule_init() {
export MY_VAR="hello"
}
zdot_simple_hook mymodule
Drop it in ~/.config/zdot-modules/ (searched automatically, ahead of the
built-in modules -- so a user module can also shadow and replace any built-in
of the same name) and add zdot_load_module mymodule to your .zshrc. The
Module Writer's Guide covers everything from here to
multi-phase plugin lifecycles, and zdot module clone <name> copies a
built-in module to your user directory as a starting point.
Contexts and Variants
Zsh runs your startup files in different scenarios -- an interactive terminal,
a script run by zsh -c, a login shell over SSH. zdot models this with
contexts: every hook declares when it should run, and zdot filters the
execution plan accordingly.
| Context | Meaning | Example scenario |
|---|---|---|
interactive | Shell attached to a user typing commands | exec zsh, opening a terminal |
noninteractive | Shell running a command or script | zsh -c '...', ssh host command |
login | First shell in a session | ssh host, macOS Terminal.app |
nonlogin | Not the session's first shell | Sub-shells, zsh inside tmux |
Variants add a third, user-defined dimension -- where the shell runs
(work vs home vs a resource-constrained Pi) -- so different hooks can
activate on different machines from one shared config.
Contexts also make it practical to use a single file as both .zshenv and
.zshrc, so scripts and ssh host command get your PATH without the
interactive overhead.
The full story -- per-context hook registration, the .zshenv symlink
pattern, and variant detection -- is in
Advanced Usage.
CLI
zdot provides an interactive CLI using a <noun> <verb> pattern with tab
completion:
zdot cache status # show cache statistics
zdot cache invalidate # clear all caches
zdot hook list # list registered hooks
zdot hook plan # print the execution plan
zdot plugin list # list plugins
zdot plugin update <name> # update a plugin
zdot module list # list loaded modules
zdot update check-updates # check for zdot updates
zdot info # environment info
zdot bench # startup benchmark
zdot profile # zprof startup profile
See docs/commands.md for the full CLI reference.
Debugging
# Verbose mode -- shows module loading, hook registration, execution order
ZDOT_VERBOSE=1 zsh
# Debug mode -- even more detail
ZDOT_DEBUG=1 zsh
# Inspect registered hooks
zdot hook list --all
# Show the execution plan and per-hook status
zdot hook plan
zdot hook status
# Full diagnostics
zdot debug
Day-to-Day
| Task | Command |
|---|---|
| Pull updates (with dotfiler) | dotfiler update |
| Add a module | Add zdot_load_module <name> to .zshrc, then exec zsh |
| Clone a built-in to customize | zdot module clone <name> |
| Clear caches after manual changes | zdot cache invalidate |
| Check startup time | zdot bench |
| Profile startup | zdot profile |
Documentation
The docs are organized as a journey -- start at the top, go as deep as you need (docs/README.md is the full index):
| Stage | Document |
|---|---|
| Install & first shell | Quickstart: dotfiler + zdot (standalone: Quick Start above) |
| Use plugins & configure shipped modules | Using Plugins |
| Write your own modules | Module Writer's Guide |
| Contexts, variants, defer, bundles | Advanced Usage |
| Reference | API ยท Dependency types ยท zstyle options ยท CLI ยท Module catalog ยท compinit |
| Internals | Implementation ยท Plugin internals |
Acknowledgements
Linting throughout this codebase is checked with shuck โ a fast shell linter with first-class zsh support. Thanks to the shuck project for catching the bugs that bash-targeted linters miss.