ADR-083: Modular ZSH Configuration Architecture
Status
Accepted - 2026-01-20
Context
The traditional monolithic ~/.zshrc file approach creates several problems:
- Complexity - Single files grow to 200+ lines, mixing unrelated concerns
- Debugging - Difficult to isolate issues to specific features
- Portability - Hard to share subsets of configuration across machines
- Maintenance - Changes risk breaking unrelated functionality
- 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
- Single Responsibility - Each module handles one concern
- Numbered Ordering - Predictable load sequence (00-99)
- Easy Toggle - Rename
.zsh→.zsh.disabledto disable - Lazy Loading - Defer expensive operations until needed
- 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 PATHfor 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
| Command | Alias | Description |
|---|---|---|
coditect-start | cds | Session orientation display |
coditect-end | cde | Export context, process pending |
coditect-search | cdf | Search context database |
coditect-pending | cdp | Show pending exports |
coditect-stats | cdx | Database 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
| File | Purpose |
|---|---|
~/.zshrc | Minimal loader (20 lines) |
~/.config/zsh/conf.d/*.zsh | Module files |
~/.config/zsh/functions/ | Autoloaded functions |
~/.zshrc.local | Private machine-specific (optional) |
~/.zshrc.backup.* | Backups before changes |
Performance Comparison
| Metric | Monolithic | Modular |
|---|---|---|
| Startup time | ~800ms | ~200ms |
| Lines in ~/.zshrc | 247 | 20 |
| Modules | 1 | 8 |
| Plaintext secrets | 1 | 0 |
References
Author: Claude Code (with Hal Casteel) Approved By: Hal Casteel Implementation Date: 2026-01-20