Skip to content

πŸͺ¨ 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:

  1. Guards β€” checks if the tool is installed ($+commands[tool])
  2. Initializes β€” runs eval "$(tool init zsh)" or equivalent
  3. 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 0 not return 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

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.)