πͺ¨ ZSHAND Hooks GuideΒΆ
π Hooks integrate external tools (mise, Atuin, Docker, etc.) into the shell. They're classified as critical (sync) or lazy (deferred) to keep startup fast.
π How Hooks WorkΒΆ
Hooks are zsh scripts in hooks/ that initialize third-party tools. Each hook:
- Guards β checks if the tool is installed (
$+commands[tool]) - Initializes β runs
eval "$(tool init zsh)"or equivalent - Configures β sets tool-specific options or aliases
β‘ Critical vs π LazyΒΆ
The framework classifies hooks into two tiers (defined in core/18_hooks.zsh):
| Tier | When | Why | Examples |
|---|---|---|---|
| β‘ Critical | Immediately at startup | Needed for first command | mise, atuin, ssh-agent |
| π Lazy | Deferred (~1s after prompt) | Not needed immediately | docker, zoxide, gitid, wrangler |
Lazy loading is enabled with ZSHAND_LAZY_LOAD=1 and moves 20β80ms of hook loading out of the startup path.
π Framework HooksΒΆ
| File | Tool | Tier | Purpose |
|---|---|---|---|
01_mise.zsh | mise | β‘ Critical | Polyglot runtime manager (node, python, ruby, etc.) |
02_atuin.zsh | Atuin | β‘ Critical | Shell history sync and search |
05_ssh-agent.zsh | ssh-agent | β‘ Critical | SSH key agent auto-start |
20_docker.zsh | Docker | π Lazy | Docker context detection and completions |
30_zoxide.zsh | zoxide | π Lazy | Smart cd with frecency |
40_wrangler.zsh | Wrangler | π Lazy | Cloudflare Workers CLI |
50_gitid.zsh | git | π Lazy | Automatic git identity switching |
80_archclean.zsh | β | π Lazy | Screenshot auto-archive (background) |
81_wlclean.zsh | β | π Lazy | Wayland temp file cleanup (background) |
π’ Numbering RangesΒΆ
| Range | Purpose | Examples |
|---|---|---|
| 01β09 | β‘ Critical / fast tools | mise, atuin, ssh-agent |
| 20β49 | π Standard integrations | docker, zoxide, wrangler |
| 50β79 | π Git / project tools | git identity |
| 80β89 | π Background jobs | archclean, wlclean |
βοΈ Writing a Custom HookΒΆ
Place custom hooks in ~/.config/zshand/hooks/:
# ~/.config/zshand/hooks/35_my_tool.zsh
# Guard: only load if tool exists
(( $+commands[my_tool] )) || return 0
# Initialize
eval "$(my_tool init zsh)"
# Optional: configure
export MY_TOOL_CONFIG="value"
π Hook PatternΒΆ
Every hook should follow this structure:
# 1. Guard clause (skip if tool not installed)
(( $+commands[tool_name] )) || return 0
# 2. Cached activation (if supported)
local cache_file="$ZSHAND_CACHE_DIR/tool_name_init.zsh"
if [[ ! -f "$cache_file" || "$cache_file" -ot "$(command -v tool_name)" ]]; then
tool_name init zsh > "$cache_file"
fi
source "$cache_file"
# 3. Tool-specific configuration
export TOOL_OPTION="value"
alias tl='tool_name list'
β‘ Performance TipsΒΆ
- Cache activation output β
eval "$(tool init zsh)"spawns a subprocess every startup. Cache the output to a file instead. - Use
return 0notreturn 1β a non-zero return from a hook stops the loading chain. - No network calls β hooks run during startup. Defer network to background.
- Pick the right number β critical tools get 01β09, everything else 20+.
π Override BehaviorΒΆ
User hooks with the same basename as framework hooks replace them:
~/.config/zshand/hooks/01_mise.zsh β replaces $ZSHAND/hooks/01_mise.zsh
~/.config/zshand/hooks/60_custom.zsh β added (no framework equivalent)
User hooks with unique basenames are loaded alongside framework hooks, sorted by number.
π§ͺ Testing HooksΒΆ
# Check if a hook's tool is detected
(( $+commands[mise] )) && echo "mise found"
# Test a hook in isolation
ZSHAND_DEBUG=1 source hooks/01_mise.zsh
# Check hook timing
ZSHAND_PROFILE_BUNDLE=1 exec zsh
# β Look for hooks section in the profile report
# Verify lazy vs critical classification
grep -A5 'critical\|lazy' core/18_hooks.zsh
π Related DocumentsΒΆ
| Document | Purpose |
|---|---|
| ποΈ ARCHITECTURE.md | Hook loading in boot sequence |
| β‘ PERFORMANCE.md | Lazy loading and hook timing |
| π€ USER_CONFIG.md | Custom hooks directory |
| π HEADER_STANDARD.md | Hook file header format |
| π external/ | Per-tool integration docs (atuin, docker, mise, etc.) |