Skip to content

ConfigΒΆ

ZSHAND Override System
Drop a file to override anything β€” framework updates never touch your config

πŸ“‹ This guide covers where every config file lives, when it runs, how overrides work, and how to extend the framework with your own code.

Structure & boot Architecture Β· Startup
Extending Core (rc.d overrides) Β· Widgets Β· Hooks
Reference Style Guide Β· Header Standard Β· Installers
Index Documentation Index

🧠 Philosophy

The framework is opinionated by default but fully overridable. You should never need to edit framework files β€” everything is customizable from your config directory. Same basename in config replaces the framework file; different name adds.

Quick start (first time)ΒΆ

  1. Create your config dir: Run zsh setup.zsh from the framework repo (or use the cloner); this creates ~/.config/zshand/ with template files.
  2. Or copy from samples: cp -r samples/user-config/* ~/.config/zshand/ then edit to taste.
  3. Set env and secrets: Edit env.zsh (EDITOR, PATH, framework flags) and secrets.zsh (API keys; keep chmod 600).
  4. Rebuild after changes: Run zr (or zfull if using full bundle) then exec zsh.
I want to… Put it here
Set env vars (EDITOR, PATH extras) env.zsh
API keys, tokens secrets.zsh only
Framework behavior (quiet, perf_budget, lazy_load) config.toml
Manage installed packages & tools packages.toml
Keybindings for widgets keybindings.toml or [keybindings] in config.toml
Replace a core module (e.g. aliases) rc.d/14_aliases.zsh (same basename)
Run before framework loads init.d/*.zsh
Run after everything post.d/*.zsh
Add a function / widget / hook functions/, widgets/, hooks/ (any name)
Add OMZ/P10k plugins plugins/ and optionally plugins.txt

When each file or directory runsΒΆ

File or directory When it runs What it's for
init.d/ Before any core module Early PATH, env, feature flags
env.zsh, secrets.zsh Early init Environment and secrets (auto-exported)
config.toml Very early (before env.zsh) Framework behavior; section.key β†’ ZSHAND_SECTION_KEY
packages.toml After config.toml (startup/18) Package lists, deactivation, aliases, tools
path.txt, bin/ During PATH build Extra PATH entries and personal scripts
rc.d/ Merged with core (02β†’90) Override or insert between core modules
hooks/, functions/, widgets/ After framework dirs of same name Your integrations; same basename = override
aliases.zsh After framework aliases Your aliases (yours win)
post.d/ After everything Final tweaks, notifications

See also: Architecture (boot sequence) Β· Startup (what runs before core)


πŸ“ Config DirectoryΒΆ

All user configuration lives under a single XDG-compliant directory:

~/.config/zshand/                   ($ZSHAND_CONFIG_DIR)

Override the location by setting ZSHAND_CONFIG_DIR in your environment before the shell starts (e.g., in ~/.zshenv).

πŸ“‚ Directory structureΒΆ

~/.config/zshand/
β”œβ”€β”€ 🌍 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 (see reference below)
β”œβ”€β”€ πŸ“¦ packages.toml       Package lists, tools, deactivation (see below)
β”œβ”€β”€ ⌨️ keybindings.toml    Widget keybindings (optional; or use [keybindings] in config.toml)
β”œβ”€β”€ πŸ“Ÿ bin/                Personal scripts (added to $PATH)
β”œβ”€β”€ πŸ› οΈ functions/         Custom zsh functions
β”œβ”€β”€ ⌨️ widgets/            Custom ZLE widgets
β”œβ”€β”€ πŸͺ¨ hooks/              Custom integration hooks
β”œβ”€β”€ βš™οΈ rc.d/               Core overrides (same basename = replace)
β”œβ”€β”€ πŸš€ init.d/             Pre-initialization scripts
β”œβ”€β”€ 🏁 post.d/             Post-initialization scripts
└── πŸ”Œ plugins/            OMZ/P10k plugins (NN-name.zsh, optional plugins.txt)

First-time setup

Run zsh setup.zsh from the framework repo to create this structure with template files, or copy from samples/user-config/. Set ZSHAND_CONFIG_DIR (e.g. in ~/.zshenv) to use a different path.


πŸ“Š Load OrderΒΆ

Understanding when each file loads helps you decide where to put things. Note: config.toml is read by core/02_vars before it sources env.zsh and secrets.zsh, so TOML values can be overridden by those files.

β”Œβ”€ Shell starts ─────────────────────────────────────────────┐
β”‚  1. πŸš€ init.d/*.zsh        Early overrides (before core)   β”‚
β”‚  2. βš™οΈ config.toml         Read by 02_vars (before env)     β”‚
β”‚  3. πŸ“¦ packages.toml       Resolve packages + deactivation  β”‚
β”‚  4. 🌍 env.zsh             Environment variables            β”‚
β”‚  5. πŸ”’ secrets.zsh         API keys (auto-exported)         β”‚
β”‚  6. πŸ›€οΈ path.txt + bin/     PATH configuration               β”‚
β”‚  7. βš™οΈ rc.d/*.zsh          Core overrides (merged with core)β”‚
β”‚  8. βš™οΈ Framework core      02_vars β†’ 04_path β†’ ... β†’ 90    β”‚
β”‚  9. πŸͺ¨ Framework hooks     Tool integrations                β”‚
β”‚ 10. πŸ› οΈ functions/          Your custom functions            β”‚
β”‚ 11. ⌨️ widgets/            Your custom widgets              β”‚
β”‚ 12. πŸͺ¨ hooks/              Your custom hooks                β”‚
β”‚ 13. πŸ“ aliases.zsh         Your aliases (can override)      β”‚
β”‚ 14. 🏁 post.d/*.zsh        Final tweaks (after everything)  β”‚
└─ Shell ready β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“¦ Package Configuration (packages.toml)ΒΆ

Controls which packages, tools, and plugins are installed and active. Uses a 4-level cascade β€” framework defaults are always present; you only need to override what you want to change.

Quick examplesΒΆ

# ~/.config/zshand/packages.toml

# Disable a tool you don't want
[packages.defaults]
atuin = false      # Skips install, sets ZSHAND_ATUIN_IS_DEACTIVATED=1
neovim = false     # Uses vim instead

# Add extra packages for your OS
[packages.ubuntu]
htop = true
glances = true

# Add tools to mise
[tools.defaults]
deno = "latest"
bun = "latest"

# Override preferences
[preferences]
js_runtime = "bun"

How deactivation worksΒΆ

When a package is set to false:

  1. It is not installed by the installer
  2. ZSHAND_<NAME>_IS_DEACTIVATED=1 is exported at startup
  3. Related hooks are skipped (e.g., hooks/02_atuin.zsh checks the env var)
  4. Related aliases fall back (e.g., bat β†’ cat, eza β†’ ls)
  5. Related widgets are unbound (e.g., fzf pickers)

Available sectionsΒΆ

Section Purpose
[packages.defaults] / [packages.<os>] System packages (apt/brew/pacman)
[tools.defaults] / [tools.<os>] Mise-managed tools (node, go, rust, uv)
[cargo.defaults] Cargo crates
[npm.defaults] npm/pnpm global packages
[pip.defaults] Python packages
[plugins.defaults] Zsh plugin git URLs
[gui.defaults] / [gui.<os>] GUI applications
[flatpak.defaults] / [flatpak.<os>] Flatpak apps
[aliases.<os>] OS-specific package name mappings
[preferences] Runtime preferences (js_runtime, python_installer, etc.)
[settings] Auto-install, logging settings
[critical] Graceful degradation actions for critical tools
[blacklist] Packages that should trigger warnings if installed
[verify] Post-install command existence checks
[updates] Update frequency per category

πŸ“Ž See samples/user-config/packages.toml for the full reference with all options documented.


🌍 Environment Variables (env.zsh)¢

Machine-specific environment variables. This file is sourced with set -a (auto-export), so every variable is automatically exported.

# env.zsh β€” Machine-specific environment variables

EDITOR="nvim"
BROWSER="firefox"
MACHINE_ID="workstation"

# Framework controls
ZSHAND_DEBUG=0
ZSHAND_LAZY_LOAD=1
ZSHAND_AUTO_FULL=1

# Tool-specific
DOCKER_HOST="unix:///run/user/1000/docker.sock"

πŸ’¬ The framework also writes ZSHAND="/path/to/zshand" into this file during setup. Don't remove it.


πŸ”’ Secrets (secrets.zsh)ΒΆ

API keys, tokens, and sensitive credentials. Automatically secured with chmod 600 during setup. Never commit this file.

# secrets.zsh β€” Sensitive credentials (chmod 600)

GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
OPENAI_API_KEY="sk-xxxxxxxxxxxx"
AWS_ACCESS_KEY_ID="AKIAXXXXXXXX"
AWS_SECRET_ACCESS_KEY="xxxxxxxxxxxxxxxx"
CLOUDFLARE_API_TOKEN="xxxxxxxxxxxxxxxx"

Security

This file is protected by chmod 600 (owner-read-only). The framework re-enforces permissions on every startup via core/06_engine.zsh. Gitleaks scans also check that secrets never leak into the repo.


πŸ“ Aliases (aliases.zsh)ΒΆ

Custom aliases loaded after framework aliases, so they can override:

# aliases.zsh β€” Custom aliases

alias ll='eza -lahF --icons'
alias gst='git status'
alias dc='docker compose'
alias k='kubectl'
alias tf='terraform'

# Override a framework alias
alias ls='eza --icons --group-directories-first'

πŸ›€οΈ PATH (path.txt)ΒΆ

Additional directories to prepend to $PATH, one per line. Lines starting with # are ignored.

# path.txt β€” Additional PATH entries

/opt/custom/bin
~/.cargo/bin
~/.npm-global/bin
~/.local/share/mise/shims

Your bin/ directory is also automatically added to $PATH.


βš™οΈ Framework settings (config.toml)ΒΆ

config.toml is read very early (before env.zsh). Each section.key is exported as ZSHAND_SECTION_KEY (e.g. [performance].perf_budget β†’ ZSHAND_PERF_BUDGET). Booleans become 1/0; ~ in paths is expanded.

Main sections (reference)ΒΆ

Section Key examples Purpose
general machine_id, theme_config, omz_theme, prezto_theme Machine ID, P10k path, theme overrides
paths log_dir, cache_dir, comp_cache Override log/cache dirs
logging json, quiet JSON logs, suppress hook warnings
performance perf_budget, auto_recompile, health_check, auto_mode Per-file budget, auto-rebuild, health check, background zprime
lazy_loading defer_compinit, lazy_load Defer compinit; defer non-critical hooks
notices show_recompile, show_perf, show_p10k_warning Stale/perf/P10k messages
quiet prime, check, sync, docs Suppress zprime/zcheck/doc output
network monitor, check_interval Atuin sync on connectivity
clipboard max_lines, log cplast limit; clipboard logging
ai explain_lines aidebug context limit
errors pre_context, post_context, keywords aidebug/cplast error detection
debug debug, timing_detail Verbose init; per-file timing
optimizations optimize, defer_mise, defer_gitid Startup speed tweaks (advanced)
tests (env-only) E2E timeouts; set via env, not TOML

Full commented example: samples/user-config/config.toml. Changes take effect on next shell (no rebuild needed).

Minimal exampleΒΆ

# config.toml

[general]
theme_config = "~/.p10k.zsh"

[performance]
perf_budget = 100
auto_mode = true

[lazy_loading]
lazy_load = true

πŸš€ Pre-Initialization (init.d/)ΒΆ

Scripts that run before the framework loads. Use this for:

  • Machine-specific overrides that must be set early
  • Environment variables that affect framework behavior
  • Workarounds that need to run before core logic
# init.d/00_machine_overrides.zsh
export ZSHAND_LAZY_LOAD=1
export ZSHAND_AUTO_FULL=1

# init.d/10_work_vpn.zsh
[[ -f /etc/vpn-active ]] && export HTTP_PROXY="http://proxy:8080"

Naming: Use NN_ prefix for load order (00 = first, 99 = last).


🏁 Post-Initialization (post.d/)¢

Scripts that run after everything else. Use this for:

  • Final overrides that need all framework functions available
  • Customizations that depend on hooks being loaded
  • Prompt tweaks that should be the absolute last thing
# post.d/00_final_env.zsh
# Set variables that depend on framework functions
export MY_PROJECT_DIR="$(git rev-parse --show-toplevel 2>/dev/null)"

# post.d/99_prompt_tweaks.zsh
# Final prompt customization (after P10k loads)
typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(dir vcs)

βš™οΈ Core Overrides (rc.d/)ΒΆ

Replace any framework core file by placing a file with the same basename in rc.d/. The framework merges both directories and user files win:

Framework: $ZSHAND/core/14_aliases.zsh       ← default
User:      ~/.config/zshand/rc.d/14_aliases.zsh  ← loaded INSTEAD

Framework: $ZSHAND/core/10_options.zsh       ← default
User:      ~/.config/zshand/rc.d/11_my_options.zsh  ← loaded ALONGSIDE (odd number)

Rules:

  • Same basename β†’ replaces framework file entirely
  • Different basename β†’ adds to load sequence (use odd numbers)

Example β€” add PATH entries between 04_path and 06_engine:

# rc.d/05_my_path.zsh β€” loaded after 04_path, before 06_engine
path+=( /opt/my-tools/bin )

πŸ› οΈ Custom Functions (functions/)ΒΆ

Drop .zsh files here to add your own functions:

# functions/greet.zsh
greet() {
    print -P "%F{green}Hello, ${1:-world}!%f"
}

These are loaded after framework functions and can override them.


⌨️ Keybindings (keybindings.toml or config.toml)¢

Widget keybindings can live in a dedicated file keybindings.toml or in a [keybindings] section of config.toml. Loaded by startup/17_az_bindkey_from_toml.zsh after default bindings.

Format: "key" = "widget_name". Key names use lowercase with hyphens: ctrl+g, alt+m, f12. Empty value unbinds the key.

# keybindings.toml (or [keybindings] in config.toml)

[keybindings]
"ctrl+g" = "aicmit"       # AI commit
"ctrl+d" = "aidebug"     # AI debug
"ctrl+p" = "pastem"     # Smart paste
"ctrl+o" = "copym"      # Copy line
"alt+m"  = "manf"       # Man page search

See Widgets for built-in widget names. After changing keybindings, reload with exec zsh (no zr needed).


⌨️ Custom widgets (widgets/)¢

ZLE widgets for keyboard shortcuts:

# widgets/my_widget.zsh
my_widget() {
    BUFFER="echo hello"
    zle accept-line
}
zle -N my_widget

Bind in keybindings.toml or [keybindings] in config.toml:

[keybindings]
"ctrl+h" = "my_widget"

πŸͺ¨ Custom hooks (hooks/)ΒΆ

Integration hooks for tools not covered by the framework:

# hooks/50_my_tool.zsh
if (( $+commands[my_tool] )); then
    eval "$(my_tool init zsh)"
fi

Use numbering (e.g. 50_) to control order; same basename as a framework hook replaces it. See Hooks.


πŸ”Œ Plugins (plugins/ and plugins.txt)ΒΆ

The framework loads plugins from ~/.config/zshand/plugins/. Files named NN-name.zsh (e.g. 50-zsh-autosuggestions.zsh) are loaded in numeric order and compiled into the full bundle. Syntax highlighting must load last β€” use prefix 99.

  • Bundle P10k config: Copy ~/.p10k.zsh to plugins/05-p10k-theme.zsh for faster startup; run zr after changes.
  • Add a plugin: Copy plugin source into plugins/NN-plugin-name.zsh or add a wrapper that sources the system path.
  • Optional plugins.txt: List plugins with prefixes: none = immediate, ~ = lazy (after first prompt), + = deferred; - = disabled. If plugins.txt exists, only listed plugins load.

Full details and OS-specific paths: samples/user-config/plugins/README.md.


πŸ“Ÿ Personal scripts (bin/)ΒΆ

Executable scripts placed here are automatically added to $PATH:

#!/usr/bin/env zsh
# bin/my_script
echo "Hello from my_script"

Make them executable: chmod +x ~/.config/zshand/bin/my_script


πŸ”„ Complete Override (zshrc.zsh)ΒΆ

For total control, create ~/.config/zshand/zshrc.zsh. This completely replaces the framework β€” nothing else loads:

# zshrc.zsh β€” Complete framework replacement
# WARNING: This disables ALL framework features
source ~/.config/zshand/env.zsh
source ~/.config/zshand/aliases.zsh
# ... your custom setup

Nuclear option

This is a full replacement β€” no framework code runs at all. Only use this if you need complete control over your shell setup.


🌍 Key environment variables¢

Set these before the framework loads (e.g. in ~/.zshenv or env.zsh). Many can also be set via config.toml (see table above).

Variable Default Purpose
ZSHAND_CONFIG_DIR ~/.config/zshand Your config directory
ZSHAND_CACHE_DIR ~/.cache/zshand Compiled bundles and cache
ZSHAND_LOG_DIR ~/logs/zshand Log files
ZSHAND_DEV_MODE 0 Development mode (no bundles)
ZSHAND_AUTO_FULL 0 Auto-compile full bundle
ZSHAND_DEBUG 0 Verbose debug output
ZSHAND_LAZY_LOAD 0 Defer non-critical hooks
ZSHAND_SAFE_MODE 0 Emergency recovery
ZSHAND_PERF_BUDGET 100 Max ms per file

❓ TroubleshootingΒΆ

Issue What to try
Config not loading Ensure ZSHAND_CONFIG_DIR points to your config dir (set in ~/.zshenv if needed). Run echo $ZSHAND_CONFIG_DIR in a new shell.
Keybinding has no effect Check widget name (e.g. pastem not paste). Reload with exec zsh. If you added a new widget, run zr so it’s in the bundle.
Changes to .zsh files not applied Run zr (or zfull if using full bundle) then exec zsh. Framework loads compiled bundles, not raw source in production.
"Bundles stale" at startup Run zr to rebuild; then exec zsh.
Secrets or env not taking effect env.zsh and secrets.zsh are sourced early; ensure no syntax errors. Use ZSHAND_DEBUG=1 exec zsh to see load order.

See Troubleshooting and Safe Mode for more.


Document Purpose
πŸ—οΈ Architecture Override system, load order, directory layout
βš™οΈ Core What rc.d/ overrides and merge order
⌨️ Widgets Built-in widgets and keybinding names
πŸͺ¨ Hooks Framework hooks and how to add your own
🎨 Style Guide Conventions for custom code
πŸ“ Header Standard Documenting your custom files
πŸ“₯ Installers Platform setup and sample templates
πŸ”§ Troubleshooting Common problems and fixes
πŸ“š Documentation Index Full doc nav