Mastering ZSH: Part 1 - Hooks and Automation
Learn how to use ZSH hooks (precmd, preexec, chpwd) to automate your shell. Includes command timing, auto-activate virtualenv, and performance tips.
- tags
- #Zsh #Shell-Scripting #Automation #Command-Line #Productivity #Terminal #Unix #Linux #Macos #Blackdot #Dotfiles #Precmd #Preexec #Chpwd
- categories
- Tutorials Shell-Scripting
- published
- reading time
- 10 minutes
📚 Series: Mastering Zsh
- Mastering ZSH: Part 1 - Hooks and Automation (current)
- Mastering ZSH: Part 2 - Line Editor and Custom Widgets
- Mastering ZSH: Part 3 - Understanding Your Prompt: How Powerlevel10k Actually Works
- Mastering ZSH: Part 4 - Completion System Demystified
ZSH hooks are built-in functions that run automatically at specific points in your shell lifecycle. Use them to automate command timing, prompt updates, virtualenv activation, and more–without plugins or performance penalties.
This guide covers all six native ZSH hook types with working examples you can paste into your .zshrc.
What ZSH Hooks Actually Do
ZSH hooks are function arrays that execute at specific lifecycle points. You register functions in these arrays and ZSH calls them automatically at the right time.
| Hook | When It Runs | Common Use Cases |
|---|---|---|
precmd_functions | Before each prompt displays | Update git status, refresh context |
preexec_functions | Before each command runs | Start timing, log commands |
chpwd_functions | After directory changes | Activate envs, load project config |
zshaddhistory_functions | Before adding commands to history | Filter secrets |
zshexit_functions | When the shell exits | Cleanup, save state |
periodic_functions | Every N seconds | Periodic checks, refresh cached data |
Execution order matters: Functions execute in array order (first to last). If one hook depends on another’s output, put it later in the array.
| |
The Six Core Hook Types
ZSH provides six commonly used built-in hook arrays.
Tip: If you’re using
$EPOCHREALTIME, load the datetime module once:
1zmodload zsh/datetime
1. precmd_functions – Before Each Prompt
Runs after a command completes but before the next prompt displays. Perfect for status updates.
| |
Use cases: Update git branch, show AWS profile, display exit status, refresh job count
2. preexec_functions – Before Each Command
Runs after you press Enter but before the command executes. Receives the command string as $1.
| |
Use cases: Command timing, lightweight logging, notifications for long commands, frequency tracking
3. chpwd_functions – After Directory Changes
Runs whenever the working directory changes via cd, pushd, popd, etc.
| |
Use cases: Auto-activate envs (Python, Node, Ruby), load project variables, update prompt context, run lightweight setup checks
4. zshexit_functions – When Shell Exits
Runs when the shell terminates.
| |
Use cases: Save persistent state, cleanup temporary files, log session duration
5. periodic_functions – Every N Seconds
Runs every $PERIOD seconds.
| |
Use cases: Background git fetch, refresh cached data, check update indicators, monitor background processes
6. zshaddhistory_functions – Before Adding to History
Runs before a command is added to history. Return 1 to skip, 0 to add.
| |
Use cases: Filter sensitive commands, skip trivial commands, deduplicate spammy entries
Using add-zsh-hook (Recommended)
add-zsh-hook offers a cleaner API and avoids some edge cases.
| |
Real-World Examples
Command Timing Display
Show timing only for commands over 5 seconds:
| |
Auto-Activate Python Virtualenv
| |
Smart Git Branch in Prompt
| |
Auto-Load Project Environment
This is simple and works–but for complex env management, direnv is safer.
| |
Filter Secrets from History
Conservative, simple filtering. Patterns are intentionally broad to avoid false negatives:
| |
Performance Considerations
Hooks run synchronously and can slow down your shell if you’re not careful. Hooks are the wrong place for network calls unless you cache the results.
Bad: Block Every Prompt
| |
Network calls on every prompt will make your shell feel broken.
Better: Background the Operation
| |
The prompt displays immediately. The background job completes later.
Best: Use Periodic Hooks
| |
Only runs every 5 minutes, not on every prompt.
Measure Hook Performance
| |
Anything consistently above ~100ms will be noticeable.
Advanced Patterns
Conditional Hook Execution
| |
Stateful Hooks
| |
Async Hooks (Experimental)
ZSH can do async patterns, but most users should reach for zsh-async for production use.
| |
This is experimental–use zsh-async if you need reliable async behavior.
Debugging Hooks
List Registered Hooks
| |
Temporarily Disable Hooks
| |
Trace Hook Execution
| |
Hook Management at Scale
File-Based Organization
~/.config/zsh/hooks/
├── precmd/
│ ├── 10-git-status.zsh
│ ├── 20-aws-profile.zsh
│ └── 90-prompt-update.zsh
├── chpwd/
│ ├── 10-venv-activate.zsh
│ └── 20-project-env.zsh
└── preexec/
└── 10-command-timing.zsh
Load them in .zshrc:
| |
A Production-Grade Hook System
If you want ordering, enable/disable control, validation, and visibility at scale, see the blackdot hook system , which provides:
- Priority-based execution (00-99 prefixes)
- Feature gating (enable/disable hooks via config)
- Validation (
blackdot hook validate) - Testing (
blackdot hook run <event>) - Visibility (
blackdot hook list)
Example:
| |
The numeric prefix (10-) controls execution order. The system handles registration automatically.
Common Pitfalls
1. Triggering Infinite Loops
| |
chpwd hooks trigger on cd, which causes another chpwd, which causes another cd…
2. Assuming Hooks Run in Scripts
Hooks are for interactive ZSH sessions. They won’t run in non-interactive shells (scripts, cron jobs).
3. Leaking State
| |
If preexec sets globals, make sure precmd clears them.
When Not to Use Hooks
Hooks aren’t always the right tool:
- Expensive operations → use cron/systemd timers
- Critical deployment steps → don’t hide them in shell lifecycle magic
- Cross-shell setups → remember Bash uses
PROMPT_COMMAND
Summary
ZSH hooks let you inject clean automation at six key points:
precmd– before prompt (update status)preexec– before command (timing, logging)chpwd– aftercd(env activation)zshexit– on exit (cleanup)periodic– every N seconds (background refresh)zshaddhistory– before history save (filter secrets)
Use add-zsh-hook for clean registration. Keep hooks fast. Cache or background anything that might block.
For structured hook management with ordering, validation, and feature gating, see the blackdot hook system documentation .
Frequently Asked Questions
What are ZSH hooks?
ZSH hooks are function arrays built into the shell that execute automatically at specific lifecycle points–before prompts display, before commands run, after directory changes, etc. You add functions to these arrays and ZSH calls them at the right time.
How do I add a hook to ZSH?
Use add-zsh-hook for the cleanest approach:
| |
Or append directly to the hook array:
| |
Why is my ZSH prompt slow after adding hooks?
Hooks run synchronously. If you’re making network calls, doing expensive computations, or running slow external commands in precmd, every prompt waits for them to complete. Solution: background slow operations with (command &) or move them to periodic hooks.
How do I auto-activate Python virtualenv when I cd into a project?
Add this to your .zshrc:
| |
How do I time long-running commands in ZSH?
Use preexec to save start time and precmd to calculate elapsed time:
| |
How do I prevent sensitive commands from being saved to history?
Use zshaddhistory to filter commands before they’re saved:
| |
Return 1 to skip saving, 0 to save.
Can I use ZSH hooks in Bash?
No, ZSH hooks are ZSH-specific. Bash has PROMPT_COMMAND for prompt-time hooks but doesn’t have equivalents for preexec, chpwd, or the other ZSH hook types. Consider switching to ZSH for full hook support.
Further Reading:
- ZSH Manual: Hook Functions
- blackdot hook system
- zsh-async - Production async hooks
Shell: ZSH 5.0+
📚 Series: Mastering Zsh
- Mastering ZSH: Part 1 - Hooks and Automation (current)
- Mastering ZSH: Part 2 - Line Editor and Custom Widgets
- Mastering ZSH: Part 3 - Understanding Your Prompt: How Powerlevel10k Actually Works
- Mastering ZSH: Part 4 - Completion System Demystified