Skip to main content

ADR-083: Modular ZSH Configuration Architecture

Status

Accepted - 2026-01-20

Context

The traditional monolithic ~/.zshrc file approach creates several problems:

  1. Complexity - Single files grow to 200+ lines, mixing unrelated concerns
  2. Debugging - Difficult to isolate issues to specific features
  3. Portability - Hard to share subsets of configuration across machines
  4. Maintenance - Changes risk breaking unrelated functionality
  5. CODITECT Integration - Framework integration scattered throughout file

Additionally, shell startup performance suffers from:

  • Eager loading of NVM (~300ms)
  • Eager loading of Pyenv (~150ms)
  • Redundant PATH entries
  • Plaintext API keys (security risk)

Decision

Implement a modular conf.d architecture with numbered loading order:

~/.config/zsh/
├── .zshrc # Minimal 20-line loader
├── conf.d/ # Auto-loaded modules (numbered)
│ ├── 00-core.zsh # Shell options, Oh-My-Zsh
│ ├── 10-path.zsh # PATH configuration
│ ├── 20-secrets.zsh # Keychain-based secrets
│ ├── 30-tools.zsh # NVM, Pyenv (lazy-loaded)
│ ├── 40-completions.zsh # FZF, GCloud, iTerm2
│ ├── 50-coditect.zsh # CODITECT framework
│ ├── 60-aliases.zsh # General aliases
│ └── 90-local.zsh # Machine-specific
└── functions/ # Autoloaded functions

Design Principles

  1. Single Responsibility - Each module handles one concern
  2. Numbered Ordering - Predictable load sequence (00-99)
  3. Easy Toggle - Rename .zsh.zsh.disabled to disable
  4. Lazy Loading - Defer expensive operations until needed
  5. Secure Secrets - Use macOS Keychain, never plaintext

Loader Implementation

# ~/.zshrc - Minimal loader
ZDOTDIR="${ZDOTDIR:-$HOME/.config/zsh}"

for conf in "$ZDOTDIR/conf.d/"*.zsh(N); do
source "$conf"
done
unset conf

The (N) glob qualifier suppresses errors if no matches found.

Module Specifications

00-core.zsh

  • Oh-My-Zsh initialization
  • Shell options (AUTO_CD, CORRECT, HIST_*)
  • History configuration

10-path.zsh

  • typeset -U PATH for deduplication
  • Homebrew detection (Apple Silicon / Intel)
  • Consolidated path array

20-secrets.zsh

  • load_secret() function for Keychain access
  • API key exports from Keychain
  • Never stores secrets in plaintext

30-tools.zsh

  • Lazy-loaded NVM - Saves ~300ms startup
  • Lazy-loaded Pyenv - Saves ~150ms startup
  • Stub functions that self-replace on first use

40-completions.zsh

  • FZF configuration and bindings
  • Google Cloud SDK completions
  • iTerm2 shell integration

50-coditect.zsh

  • CODITECT_HOME and CODITECT_ROLLOUT exports
  • Router configuration (cr, cri aliases)
  • Workflow commands (cds, cde, cdf, cdp, cdx)
  • Health check function

60-aliases.zsh

  • Modern CLI replacements (eza, bat)
  • Git shortcuts
  • Navigation aliases

90-local.zsh

  • Machine-specific overrides
  • Sources ~/.zshrc.local if exists
  • Loads last to allow overrides

CODITECT Workflow Commands

CommandAliasDescription
coditect-startcdsSession orientation display
coditect-endcdeExport context, process pending
coditect-searchcdfSearch context database
coditect-pendingcdpShow pending exports
coditect-statscdxDatabase statistics
coditect-health-Installation health check

Consequences

Positive

  • Maintainability - Edit 30-line modules, not 250-line files
  • Debuggability - Disable individual modules to isolate issues
  • Performance - ~500ms faster startup with lazy loading
  • Security - API keys in Keychain, not plaintext
  • Portability - Share specific modules across machines
  • CODITECT Integration - Dedicated module with workflow commands

Negative

  • Indirection - Must navigate to conf.d/ to edit
  • Discovery - New users must learn structure
  • Dependency - Modules may have implicit dependencies

Mitigations

  • Clear numbering indicates load order
  • README in conf.d/ documents each module
  • 90-local.zsh provides escape hatch

How-To Guide

Edit a Module

code ~/.config/zsh/conf.d/50-coditect.zsh

Disable a Module Temporarily

mv ~/.config/zsh/conf.d/30-tools.zsh ~/.config/zsh/conf.d/30-tools.zsh.disabled
exec zsh # Reload

Re-enable a Module

mv ~/.config/zsh/conf.d/30-tools.zsh.disabled ~/.config/zsh/conf.d/30-tools.zsh
exec zsh

Add a New Module

# Create module with appropriate number for load order
cat > ~/.config/zsh/conf.d/45-docker.zsh << 'EOF'
# Docker configuration
alias d='docker'
alias dc='docker-compose'
EOF
exec zsh

Add a Secret to Keychain

# Store
security add-generic-password -a "$USER" -s "OPENAI_API_KEY" -w "sk-..."

# Update existing
security add-generic-password -a "$USER" -s "OPENAI_API_KEY" -w "sk-new..." -U

# Verify
security find-generic-password -a "$USER" -s "OPENAI_API_KEY" -w

Debug Slow Startup

# Time each module
for f in ~/.config/zsh/conf.d/*.zsh; do
SECONDS=0
source "$f"
echo "$SECONDS $(basename $f)"
done | sort -rn | head -5

Rollback to Monolithic

cp ~/.zshrc.backup.20260119-pre-modular ~/.zshrc
exec zsh

File Locations

FilePurpose
~/.zshrcMinimal loader (20 lines)
~/.config/zsh/conf.d/*.zshModule files
~/.config/zsh/functions/Autoloaded functions
~/.zshrc.localPrivate machine-specific (optional)
~/.zshrc.backup.*Backups before changes

Performance Comparison

MetricMonolithicModular
Startup time~800ms~200ms
Lines in ~/.zshrc24720
Modules18
Plaintext secrets10

References


Author: Claude Code (with Hal Casteel) Approved By: Hal Casteel Implementation Date: 2026-01-20