ποΈ ZSHAND ArchitectureΒΆ
π― Read this first if you're new to the codebase. This document explains how the framework is structured, how it boots, how bundles and overrides work, and where your config plugs in.
TL;DR
Entry: zshrc.zsh β shared_functions + startup β core (02β90) β hooks β user post.d.
Speed: Use a full bundle (ZSHAND_AUTO_FULL=1 or zfull) for <50 ms cold start.
Override: Same basename in ~/.config/zshand/rc.d/ (or hooks/functions/widgets) replaces the framework file.
π§ Design principles
- β‘ Performance first β target <50 ms cold start, <10 ms warm
- π§© Modular β every directory is independently compilable
- π Override-friendly β users replace any framework file by dropping a file in config
- π‘οΈ Fail-safe β safe mode and graceful degradation at every layer
- π Convention-driven β numbered prefixes enforce load order; no magic, no plugins
π Related documentsΒΆ
| Config & boot | Config Β· Startup Β· Safe Mode |
| Build & perf | Build System Β· Performance Β· Auto-Full |
| Reference | Core Β· Hooks Β· Header Standard |
| Index | Documentation Index Β· Home |
In this doc: How it's structured β Directory structure β Boot sequence β zshrc entry point β Bundles β Overrides β Key subsystems β Hooks β Env vars.
How it's structured and works togetherΒΆ
The diagram above shows the five layers: User space (your config dir), Startup pipeline (shared_functions + startup scripts), Core modules (variables β path β engine β β¦ β functions), Extensions (hooks, then functions and widgets), and Post-init (your post.d scripts). One entry point β zshrc.zsh β runs them in a fixed order so behavior is predictable and overridable.
Why itβs built this way:
- Single entry point β Everything goes through
zshrc.zsh. No plugin manager, no magic; you can trace exactly what runs and when. - Layers β Primitives (shared_functions) load first; then startup (TOML, safe mode, health); then core (config and engine); then extensions (hooks, functions, widgets). Your config is merged in at defined points (rc.d with core, separate dirs for hooks/functions/widgets), so framework updates never overwrite you.
- Bundles β Instead of sourcing dozens of files every time, the build system compiles directories (or the whole stack) into bytecode. One read, one parse: cold start drops from hundreds of ms to tens. The βhowβ (which strategy: full bundle, per-dir, or individual files) is chosen at boot.
- Overrides β Same basename in your config replaces the framework file; different name adds. You get full control without forking.
The image summarizes this: your config and the framework meet at clear boundaries (init.d, rc.d, post.d, and the parallel dirs for functions/widgets/hooks). The sections below fill in directory layout, boot sequence, bundle types, and override rules so you can find things and change behavior.
π Directory structureΒΆ
The repo is a flat set of directories under one root. Each directory has a single responsibility and is compiled to its own bundle (or included in the full bundle). Entry point: zshrc.zsh (sourced by ~/.zshrc). Bootstrap only: setup.zsh (first-time install and OS setup).
At a glanceΒΆ
| Directory / file | Role | When / how it loads | User override? |
|---|---|---|---|
| zshrc.zsh | Entry point | Every interactive shell | No (sourced by your .zshrc) |
| setup.zsh | First-time install | Manual / installer only | No |
| shared_functions/ | Primitives (stderr, timing, load) | Before core; same pass as startup | No (framework-only) |
| startup/ | TOML, safe mode, health, dev check | Before core; same pass as shared_functions | No |
| core/ | Variables, PATH, engine, options, aliases, widgets, hooks loader, plugins, functions | After startup; 02β90 in order; merged with user rc.d/ | Yes β same basename in rc.d/ replaces |
| functions/ | User-facing shell utilities | After core; merged with user functions/ | Yes β same basename replaces |
| widgets/ | ZLE keybinding widgets | After core; merged with user widgets/ | Yes β same basename replaces |
| hooks/ | Tool integrations (mise, atuin, docker, β¦) | After core; merged with user hooks/ | Yes β same basename replaces |
| bin/ | CLI executables on $PATH | PATH built from here (and user bin/) | Yes β user bin/ in config |
| build/ | Compile, profile, load logic | Used at runtime only when not using a full bundle | No |
| private_functions/ | Internal helpers | Loaded by core/engine when needed | No |
| lib/ | Rebuild logic, support code | Used by build/ and engine | No |
| config/ | Framework config (e.g. profile exceptions) | Read by build/profile scripts | No |
| installers/ | OS-specific install scripts | Run by setup or manually | No |
| tests/ | ShellSpec specs | just test | No |
| docs/ | This documentation | β | No |
| samples/ | Example user configs | Reference only | No |
Where to find or change whatΒΆ
| You want to⦠| In repo | In config |
|---|---|---|
| Change PATH | core/04_path.zsh | path.txt, bin/, or rc.d/04_*.zsh |
| Change aliases | core/14_aliases.zsh | aliases.zsh or rc.d/14_aliases.zsh |
| Add a shell function | functions/*.zsh | ~/.config/zshand/functions/ |
| Add a keybinding widget | widgets/*.zsh | ~/.config/zshand/widgets/ |
| Add a tool hook (mise, docker, β¦) | hooks/*.zsh | ~/.config/zshand/hooks/ |
| Run code before core | β | init.d/*.zsh |
| Run code between core modules | β | rc.d/NN_name.zsh (same numbering as core) |
| Run code after everything | β | post.d/*.zsh |
| Rebuild / compile | build/*.zsh, core/06_engine.zsh | β |
Full config layout: Config.
Tree by roleΒΆ
Entry and bootstrap
zshand/
βββ zshrc.zsh Entry point (sourced by ~/.zshrc)
βββ setup.zsh First-time bootstrap & OS setup (installers use this)
Primitives and startup (load before core)
shared_functions/ Low-level utilities β stderr, timing, bytecode load, clipboard
β βββ 01_stderr_error.zsh
β βββ 06_time_millis.zsh
β βββ 11_source_with_zwc.zsh
β βββ 15_load_file_timed.zsh
β βββ ...
startup/ Early pipeline β dev check, TOML, safe mode, health
β βββ 05_az_dev_mode_check.zsh
β βββ 21_az_health_validate_tools.zsh
β βββ 24_az_safe_mode.zsh
β βββ ...
Core pipeline (02β90; user rc.d/ merged by basename)
core/ Framework core β one module per concern
β βββ 02_vars.zsh Variables & identity (first in core)
β βββ 04_path.zsh PATH construction
β βββ 06_engine.zsh Build engine (zprime, zr, zfull), zrun, completion
β βββ 08_audit.zsh Integrity & telemetry
β βββ 10_options.zsh Zsh options (setopt)
β βββ 12_completions.zsh Completion system
β βββ 14_aliases.zsh Shell aliases
β βββ 16_widgets.zsh ZLE widget registration
β βββ 18_hooks.zsh Hook classification & loader
β βββ 20_plugins.zsh Plugin management
β βββ 90_functions.zsh Function autoloading
Extensions (user-facing; merged with config dirs)
functions/ Shell utilities (add_path, health, ztop, β¦)
widgets/ ZLE widgets (paste, AI commit, git branch, β¦)
hooks/ Tool hooks (mise, atuin, docker, zoxide, β¦)
bin/ CLI tools on PATH (zr, zfull, aicontext, β¦)
Build and internal
build/ Compilation, profiling, load strategy
β βββ compile_full_bundle.zsh
β βββ compile_profile_bundle.zsh
β βββ profile_bundle_report.zsh
β βββ ...
private_functions/ Internal helpers (not for user config)
lib/ Support (e.g. lib/rebuild/ for incremental compile)
config/ Framework config (e.g. profile-exceptions.conf)
Project and docs
installers/ OS-specific system installers
tests/ ShellSpec test suites
docs/ Documentation (this file)
samples/ Example user config templates
π Numbering conventionΒΆ
Numbered directories (core/, shared_functions/, startup/, hooks/) use 2-digit prefixes so load order is deterministic. Numbers are per-directory (e.g. core/02_vars is first in core; hooks/01_mise is first in hooks).
| Range | Typical use | Examples |
|---|---|---|
01β09 | Foundation / bootstrap | 02_vars.zsh, 01_mise.zsh (hooks) |
10β19 | Core config / options | 10_options.zsh, 14_aliases.zsh |
20β29 | Plugins & integrations | 20_plugins.zsh, 20_docker.zsh |
30β49 | Mid-priority tools | 30_zoxide.zsh |
50β79 | Optional / less critical | 50_gitid.zsh |
80β89 | Cleanup / maintenance | 80_archclean.zsh |
90β99 | Late-stage / finalization | 90_functions.zsh |
Even numbers = framework. Odd numbers = reserved for user insertion (e.g. ~/.config/zshand/rc.d/03_my_vars.zsh loads between 02_vars and 04_path). See Override system.
π Boot SequenceΒΆ
zshrc.zsh is the single entry point. It decides how to load (bundle vs individual files), then runs a fixed pipeline. Non-interactive shells (scripts, cron) exit early; interactive shells run the full sequence.
π― zshrc.zsh entry pointΒΆ
What it is: The one file your ~/.zshrc sources. Typically your .zshrc contains only:
What it does:
| Responsibility | Brief |
|---|---|
| Gate | Exits immediately for non-interactive shells (scripts, cron, scp) so the framework never runs there. |
| Bootstrap | Sets ZSHAND, ZSHAND_CACHE_DIR, ZSHAND_CONFIG_DIR and related paths so every script can find the repo and your config. |
| Strategy | Chooses loading mode: full bundle, per-directory bundles, or individual files (see Loading strategies). |
| Pipeline | Runs the 10-step sequence below: P10k, safe mode, health, core (with your rc.d merged), hooks, staleness check, your post.d, cleanup. |
| No plugin manager | There is no second entry point. Everything is traceable from this file. |
Where it lives: At the framework root, next to setup.zsh. The installer or cloner places the repo and points your .zshrc at zshrc.zsh.
Pipeline (high level)ΒΆ
| Step | What runs | Why it matters |
|---|---|---|
| 0 | Non-interactive check | Scripts and cron skip the framework entirely. |
| 1 | Path & cache init | ZSHAND, ZSHAND_CACHE_DIR, etc. so everything else can find files. |
| 2 | Loading strategy | Chooses full bundle, hybrid (per-dir bundles), or individual files. See Loading strategies below. |
| 3 | P10k instant prompt | Prompt appears immediately; rest of init can take time without blocking the UI. |
| 4 | Safe mode check | If init failed 3+ times recently (or ZSHAND_SAFE_MODE=1), drop into recovery shell and stop. |
| 5 | Health validation | Require git, zsh, etc.; write to failure tracker if something is missing. |
| 6 | Core (02β90) | Variables, PATH, engine, audit, options, completions, aliases, widgets, hooks loader, plugins, functions. User rc.d/ is merged in by basename. |
| 7 | Hooks | Critical hooks sync; lazy hooks can be deferred if ZSHAND_LAZY_LOAD=1. |
| 8 | Staleness & recompile | If bundles are older than source, set flag; optional auto zprime --quiet in dev. |
| 9 | User post.d/ | Your scripts run last; ideal for one-off tweaks. |
| 10 | Debug summary & cleanup | Unset _az_* startup functions, print timing if ZSHAND_DEBUG=1. |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β zshrc.zsh Entry Point β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 0. π« Non-interactive check (early exit for scripts/cron) β
β 1. π Path & cache initialization β
β 2. π Loading strategy selection (see below) β
β 3. π¨ P10k instant prompt (zero-latency visual) β
β 4. π‘οΈ Safe mode check (emergency recovery) β
β 5. β
Health validation (system dependencies) β
β 6. βοΈ Core (02β90) + user rc.d/ merged β
β 7. πͺ¨ Hooks (critical sync, then lazy/deferred) β
β 8. π Staleness check & optional auto-recompile β
β 9. π€ User post.d/ scripts β
β 10. π Debug summary & cleanup β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Loading strategiesΒΆ
The framework picks the fastest available strategy that matches your environment (e.g. dev mode forces individual files). In production, the full bundle is fastest.
| Priority | Strategy | When it's used | Typical speed |
|---|---|---|---|
| 1 | Profile bundle | ZSHAND_PROFILE_BUNDLE=1 | ~100 ms (instrumented) |
| 2 | Full bundle (fast path) | ZSHAND_AUTO_FULL=1 and bundle exists | <50 ms |
| 3 | Full bundle (first run) | ZSHAND_AUTO_FULL=1 but no bundle yet | ~200 ms once, then fast |
| 4 | Monolithic | ZSHAND_USE_MONOLITHIC=1 (framework only) | ~60 ms |
| 5 | Hybrid | Default: per-directory .compiled/*.zwc | ~80 ms |
| 6 | Individual files | ZSHAND_DEV_MODE=1 or fallback | ~200 ms (best for debugging) |
Which mode am I in?
Run echo $ZSHAND_DEV_MODE $ZSHAND_AUTO_FULL. In production with auto-full, one cached file is sourced from $ZSHAND_CACHE_DIR β no build code loaded, no per-dir reads.
π¦ Bundle SystemΒΆ
Why bundles? Sourcing dozens of .zsh files costs time; one pre-compiled file (or one per directory) loads in a single read and parse. The build system concatenates sources (with user overrides merged in), then zcompiles to .zwc bytecode. Result: 3β5Γ faster load per directory, and with a full bundle the whole framework + your config loads in one shot.
Bundle typesΒΆ
| Bundle | Where it lives | Contains | Built by |
|---|---|---|---|
| Full | $ZSHAND_CACHE_DIR/zshand-full.zsh + .zwc | Framework + user (init.d, rc.d, post.d, functions, widgets, hooks, aliases) | zfull / compile_full_bundle.zsh |
| Profile | Same dir, zshand-profile.* | Full bundle + timing instrumentation | ZSHAND_PROFILE_BUNDLE=1 + compile |
| Per-directory | <dir>/.compiled/<dir>-all.zsh + .zwc | One directoryβs files only | zprime / compile_single_directory.zsh |
How compilation worksΒΆ
flowchart LR
subgraph inputs[" "]
A[Source files\nframework + user]
end
subgraph merge[" "]
B[Merge by basename\nuser replaces framework]
end
subgraph build[" "]
C[Concatenate\nload order]
D[one .zsh]
E[zcompile]
F[.zwc bytecode]
end
A --> B --> C --> D --> E --> F User files in rc.d/, functions/, widgets/, hooks/ that share a basename with a framework file are replaced in the bundle; unique names are added. See Override system.
CommandsΒΆ
| Command | What it does |
|---|---|
zr / zprime | Rebuild all per-directory bundles (and optionally full). Reload shell. |
zprime --quiet | Same, no output (e.g. background). |
zfull / zprime --full | Build full bundle (framework + user) in cache. |
zr-dir <dir> / zprime-dir <dir> | Rebuild one directory only. |
zrecompile user | Rebuild only user-related parts of the full bundle (faster after config-only changes). |
Stale bundles
If you edit .zsh files and donβt rebuild, the old bytecode is still loaded. Youβll see a βstaleβ warning at startup. Run zr or zfull (and exec zsh) to refresh.
π Override SystemΒΆ
You can replace or extend the framework without forking. Same basename in your config = replaces the framework file. Different name = adds (both load). Framework updates never overwrite your config.
Example: To replace the frameworkβs aliases entirely, add ~/.config/zshand/rc.d/14_aliases.zsh. The loader will use your file instead of $ZSHAND/core/14_aliases.zsh. To add aliases on top, use ~/.config/zshand/aliases.zsh (loaded after core).
Resolution ruleΒΆ
Framework: $ZSHAND/core/14_aliases.zsh
User: $ZSHAND_CONFIG_DIR/rc.d/14_aliases.zsh β wins (same basename)
Same rule applies in functions/, widgets/, hooks/: same basename β replace; new name β add. Full layout: Config.
User config directoryΒΆ
~/.config/zshand/ ($ZSHAND_CONFIG_DIR)
βββ env.zsh π Environment variables (auto-exported)
βββ secrets.zsh π API keys and tokens (chmod 600)
βββ aliases.zsh π Custom aliases (loaded after framework)
βββ path.txt π€οΈ Additional PATH entries (one per line)
βββ config.toml βοΈ Framework behavior (dev_mode, quiet, perf_budget, etc.)
βββ bin/ π Personal scripts (on $PATH)
βββ functions/ π οΈ Custom functions (override or extend)
βββ widgets/ β¨οΈ Custom ZLE widgets
βββ hooks/ πͺ¨ Custom tool hooks (e.g. 60_my_tool.zsh)
βββ rc.d/ βοΈ Core overrides (same numbering as core/, e.g. 14_aliases.zsh)
βββ init.d/ π Before framework (early scripts)
βββ post.d/ π After everything (final tweaks)
User file load orderΒΆ
| Order | What loads | Notes |
|---|---|---|
| 1 | init.d/*.zsh | Before any framework core. |
| 2 | env.zsh, secrets.zsh | Environment and secrets. |
| 3 | path.txt, bin/ | PATH. |
| 4 | Core (02β90) | Merged with rc.d/: user file same basename replaces framework. |
| 5 | Framework then user hooks/, functions/, widgets/ | Each dir: framework first, then user (user can override by basename). |
| 6 | aliases.zsh | Loaded after framework aliases; can override. |
| 7 | post.d/*.zsh | Last; your final tweaks. |
βοΈ Key SubsystemsΒΆ
π§ Engine (core/06_engine.zsh)ΒΆ
The engine is the largest core module and the one that makes βrebuild and reloadβ work. It defines:
zprime/zrβ Compile directories (or full bundle), optionally reload shellzrunβ Run commands with optional telemetry logging_z_system_initβ Post-init: Atuin, terminal title, security checks- Incremental rebuild β Only recompile dirs that changed (or depend on changed dirs)
- Completion generation β Atomic completion for external CLIs
If youβre asking βwhere does zr come from?β or βwho builds the bundle?β β itβs here. See Build System for the full pipeline.
π§± Shared functions (shared_functions/)ΒΆ
Lowest-level primitives, loaded in the same pass as startup/ (alphabetically). No core/ or user code runs before these. Used by startup scripts, init.d, post.d, and rc.d.
| Function | Purpose |
|---|---|
stderr_error / stderr_warn | Colored stderr output |
echo_info / echo_ok | Status messages |
time_millis | Millisecond timing |
assert_*_exists | Guard assertions |
source_with_zwc | Prefer .zwc when present |
load_file_timed | Source with timing and perf budget |
_zrun | Telemetry logging (used by zrun) |
clipboard_copy / clipboard_paste | Clipboard (Wayland/X11/macOS) |
Shared Functions has the full list.
π‘οΈ Safe mode (startup/24_az_safe_mode.zsh)ΒΆ
If the shell fails to init 3+ times in a short window (or you set ZSHAND_SAFE_MODE=1), the next start drops into safe mode: minimal PATH, no framework, recovery menu. Lets you fix a broken config without losing shell access. See Safe Mode.
π Profiling pipelineΒΆ
ZSHAND_PROFILE_BUNDLE=1 exec zsh β instrumented bundle built & run
β
profile-timing.log β raw TYPE|NAME|START|END
β
profile_bundle_report.zsh β graded report (AβF, bar charts)
Use when tracking down slow startup. Performance and Profile Report Guide have details.
πͺ¨ Hook classificationΒΆ
Hooks (tool integrations) are classified in core/18_hooks.zsh so the loader can run some at once and defer others:
| Type | When they load | Use for | Examples |
|---|---|---|---|
| Critical | Synchronous, before first prompt | Things the prompt or early commands need | 01_mise, 02_atuin, 05_ssh-agent |
| Lazy | After prompt, or after 1s delay if ZSHAND_LAZY_LOAD=1 | Tools that can wait | 20_docker, 30_zoxide, 50_gitid |
With ZSHAND_LAZY_LOAD=1, non-critical hooks run in a background subshell after 1 second, so startup stays fast. Add your own in ~/.config/zshand/hooks/ (e.g. 60_my_tool.zsh). Full list and ordering: Hooks.
π§ͺ Testing architectureΒΆ
Tests live under tests/ and spec/ (ShellSpec). Specs mirror the source tree: e.g. functions/dcopy.zsh β spec/functions/dcopy_spec.sh. Run just test (or shellspec). CI uses Kcov for coverage. See Testing.
π Environment variablesΒΆ
These control where the framework looks for files, whether to use bundles, and how noisy or strict to be. Set before the framework loads (e.g. in ~/.zshenv for ZSHAND_AUTO_FULL).
Framework controlsΒΆ
| Variable | Type | Default | Purpose |
|---|---|---|---|
ZSHAND | string | auto-detected | Framework root directory |
ZSHAND_CONFIG_DIR | string | $XDG_CONFIG_HOME/zshand | User config directory |
ZSHAND_CACHE_DIR | string | $XDG_CACHE_HOME/zshand | Cache & compiled bundles |
ZSHAND_LOG_DIR | string | $HOME/logs/zshand | Log file directory |
ZSHAND_DEV_MODE | bool | 0 | Enable development mode |
ZSHAND_AUTO_FULL | bool | 0 | Auto-compile full bundle |
ZSHAND_DEBUG | bool | 0 | Verbose debug output |
ZSHAND_PROFILE_BUNDLE | bool | 0 | Enable profiling |
ZSHAND_LAZY_LOAD | bool | 0 | Defer non-critical hooks |
ZSHAND_SAFE_MODE | bool | 0 | Emergency recovery mode |
ZSHAND_AUDIT_MODE | bool | 0 | Full audit (recompile + test) |
ZSHAND_PERF_BUDGET | int | 100 | Max ms per file before warning |
π XDG ComplianceΒΆ
The framework follows the XDG Base Directory Specification:
| XDG Variable | ZSHAND Maps To | Default |
|---|---|---|
XDG_CONFIG_HOME | ZSHAND_CONFIG_DIR | ~/.config/zshand |
XDG_CACHE_HOME | ZSHAND_CACHE_DIR | ~/.cache/zshand |
XDG_DATA_HOME | β | (not currently used) |
π Dependency graphΒΆ
zshrc.zsh
βββ shared_functions/* (first β primitives; no core yet)
βββ startup/* (same pass as shared_functions, alphabetical merge)
βββ [if not bundle] build/detect_dev_mode.zsh
βββ core/02_vars.zsh (bootstrap; no other core deps)
β β 04_path β 06_engine β 08_audit β 10_options β β¦ β 90_functions
βββ hooks/* (after core; order from 18_hooks.zsh)
βββ P10k theme
βββ post.d/* (last)
No circular deps
shared_functions/ and startup/ load before any core/. Within core/, files run in numeric order only; a file must not depend on a higher number in the same directory.