Mastering Zsh: A Complete Guide to Configuration, Plugins, and Developer Productivity
Quick summary: This Mac runs Zsh 5.9. Upstream published the next patch on 12 July 2026. A minimal zshrc took 48.2 ms median over 20 warm starts, against 4.2 ms with no rc files.
Key Takeaways
- This Mac runs Zsh 5.9
- Upstream published the next patch on 12 July 2026
- A minimal zshrc took 48.2 ms median over 20 warm starts, against 4.2 ms with no rc files
- On 11 October 2026 on this Mac was Zsh 5.9 ( )
- The upstream production tarball is 5.9 patch 2, published 12 July 2026 at zsh

Table of Contents
On 11 October 2026 /bin/zsh on this Mac was Zsh 5.9 (arm64-apple-darwin26.0). The upstream production tarball is 5.9 patch 2, published 12 July 2026 at zsh.org/pub. macOS has not caught up. The manual checked for this page is the Zsh 5.9 release manual, including startup files, the line editor, the completion system, and options.
A shell with no rc files (zsh -f -c exit) had a median of 4.2 ms over 20 runs. The same binary, after one warmup, loading only minimal.zshrc from a temporary ZDOTDIR, had a median of 48.2 ms (minimum 46.2, maximum 72.5). That is one workstation, uname Darwin 27.0 arm64. It is not a budget for your laptop.
What broke —
/bin/sh -c 'print $ZSH_VERSION'exited 127 with/bin/sh: print: command not found.ZSH_VERSIONare Zsh. The lab script’s own process wasbashwhileSHELLwas/bin/zsh. Copying a.zshrcalias into a#!/bin/shscript fails the same way.
Reproduce this — From a checkout of this site, run
bash examples/architecture-blog-2026/mastering-developer-tools/mastering-zsh/check-shell.sh. On 11 October 2026 it printedzsh_version=zsh 5.9 (arm64-apple-darwin26.0),sh_print_exit=127,bare_median_ms=4.2,minimal_rc_median_ms=48.2, andlab=ok. The temporary directory is removed on exit. Published copy: /examples/architecture-blog-2026/mastering-developer-tools/mastering-zsh/check-shell.sh.
We recommend learning the builtins, then adding one plugin loader and a short list: completions, autosuggestions, history search, and one syntax highlighter, plus fzf, zoxide, and Starship if you want them. The trade-off is that you do not get Oh My Zsh’s alias catalog on day one. You also do not get two frameworks fighting over Ctrl-R.
Zsh does not replace ls, the terminal emulator, Homebrew or apt, or kubectl. It is the program those tools run inside.
What Zsh is
Zsh is a Unix shell: an interactive command language and a scripting language. It was first written in 1990. The production release published for download on 12 July 2026 is 5.9 patch 2. The binary on this Mac is still the system 5.9.
Bash is the shell most Linux servers start for scripts, and the language of a large amount of existing automation. Zsh is the default login shell on current macOS. People install it on Linux because the interactive features are ahead of the Bash that ships as /bin/sh on older images. Neither shell is a terminal. Terminal, iTerm2, Ghostty, and the Windows Terminal draw the window. Zsh is the process inside it.
Features worth learning before any plugin:
- Filename generation, including recursive
**/and qualifiers such as(.)for regular files. - A completion system (
compinit) that is programmable per command. - An interactive history with timestamps, sharing, and a space prefix that skips one entry.
PROMPTandRPROMPT, edited with prompt escapes, not with a theme language.- Arrays that are 1-indexed unless
KSH_ARRAYSis set, and associative arrays. - Functions, the line editor (
zle), andbindkey.
| Topic | Zsh | Bash |
|---|---|---|
| Scripts | #!/usr/bin/env zsh. Not a valid #!/bin/sh. | #!/usr/bin/env bash for Bash, #!/bin/sh for POSIX. |
| Interactive defaults | Completion, glob qualifiers, spelling correction if you turn it on. | Readline, programmable completion if bash-completion is installed. |
| Startup files | .zshenv, .zprofile, .zshrc, .zlogin, .zlogout. | .bash_profile or .bashrc, depending on login and interactive. |
| Arrays | 1-indexed. typeset -A for associative arrays. | 0-indexed. Associative arrays need Bash 4. |
| Globs | Unmatched patterns error. In a non-interactive script the shell exits 1. | Unmatched patterns stay literal unless failglob or nullglob is set. |
| Completion | compinit, then compdef. | complete and compgen. |
| Portability | Installed by default on macOS. A package elsewhere. | Common on Linux. macOS /bin/bash can be older than a Homebrew Bash. |
| Typical job | The shell you type in. | Scripts, CI, and images that already have Bash. |
Do not paste .zshrc into a Bash script. Aliases, print, setopt, glob qualifiers, and ZSH_VERSION are not a shared language. A function you want in both shells has to be written twice, or written as POSIX sh and tested with sh.
Install Zsh and choose the login shell
Installing the package and changing the login shell are different steps. Confirm the binary before chsh.
Context: this Mac, Zsh 5.9, read-only. zsh -f skips rc files.
zsh --version
command -v zsh
echo "$ZSH_VERSION"
ps -p $$ -o comm=
echo "$SHELL"zsh --version and command -v zsh name the binary you would execute. ZSH_VERSION is set only inside Zsh. ps names the current process. SHELL is the login shell recorded for your user. The lab script was bash and still printed SHELL=/bin/zsh.
Where the system lists login shells, read that file. Do not append to it until the path you want is a real binary.
grep zsh /etc/shellsOn this Mac, /etc/shells contains /bin/zsh.
macOS. Zsh is already the login shell on current releases. /bin/zsh here is 5.9. Install a newer Zsh with Homebrew only if you need 5.9.2 or later, and do not chsh to the Homebrew path until that path is listed in /etc/shells. Two Zsh binaries on PATH is a normal way to get confused. whence -a zsh shows each one.
Debian and Ubuntu. sudo apt update and sudo apt install zsh. These commands were not run for this page.
Fedora and RHEL. sudo dnf install zsh.
Arch. sudo pacman -S zsh.
WSL. Install Zsh inside the Linux distribution with that distribution’s package manager. The Windows Terminal profile chooses the program that starts. A profile that launches bash will not read .zshrc, even if chsh inside the distro succeeded. Open a tab with that profile and check echo $ZSH_VERSION.
Already installed. Stop. command -v zsh is enough. A second install from a curl script does not make the shell newer than the binary you actually exec.
Set the login shell only after the path is in /etc/shells:
chsh -s "$(command -v zsh)"Open a new terminal. echo $ZSH_VERSION should print a version. To go back, chsh -s to the previous path from /etc/shells, often /bin/bash on Linux. On this Mac the login shell is already Zsh, so there is nothing to switch. chsh was not run here.
A syntax error in .zshenv breaks Zsh scripts as well as terminals, because every Zsh reads that file. zsh -f starts without rc files so you can edit them.
Startup files
From the Files chapter, read on 11 October 2026. If ZDOTDIR is unset, $HOME is used. /etc can be a different directory in some builds. RCS and GLOBAL_RCS are on by default. Unsetting RCS stops later startup files. zsh -f does that.
Every Zsh process:
/etc/zshenv$ZDOTDIR/.zshenv
Login shells then:
/etc/zprofile$ZDOTDIR/.zprofile
Interactive shells then:
/etc/zshrc$ZDOTDIR/.zshrc
Login shells then:
/etc/zlogin$ZDOTDIR/.zlogin
When a login shell exits, $ZDOTDIR/.zlogout runs, then /etc/zlogout. exec of another program skips the logout files. If RCS is unset at exit, history is not written.
A script (zsh script.zsh, or zsh -c) is usually non-interactive and not a login shell. It reads the zshenv files and does not read .zshrc. That is why an alias in .zshrc is invisible to scripts, and why a heavy eval in .zshenv slows every script and every make recipe that calls Zsh.
Put interactive pieces in .zshrc: aliases, compinit, the prompt, plugin loading, fzf, zoxide, Starship. Put the smallest possible PATH adjustment in .zshenv, and only if scripts need it. Put login-only environment in .zprofile (for example a login-time PATH you do not want reapplied on every subshell).
.zlogin runs after .zshrc for login shells. People who set the prompt in both files discover that .zlogin wins on the first terminal and .zshrc wins on nested shells. Pick .zshrc for the prompt.
A maintainable layout is three files plus the plugin list, not a tree of snippets:
.zshenvfor exported variables scripts need.zprofilefor login-only setup.zshrcfor interactive setup.zsh_plugins.txtif you use Antidote
Split further when two people edit the same .zshrc and the diffs hurt. Until then a second directory is another place for a source to fail at startup.
source of a missing file aborts .zshrc at that line when NOMATCH or a failed source stops the rest. Guard optional tools with [[ -r file ]] or command -v.
Built-in productivity
Context: Zsh 5.9, zsh -f unless a block says it uses the minimal rc. These commands were run on this Mac, or they are options setopt accepted under zsh -f.
Navigation
pwd prints the current directory. pwd -P resolves symlinks. cd changes it. ~ is your home directory. A path that starts with / is absolute.
The directory stack is built in. pushd /tmp goes there and remembers the previous directory. popd returns. dirs -v lists the stack. That is enough for two or three working trees. zoxide, later, is for dozens.
Quote names that contain spaces.
mv -- 'my notes.txt' ./archive/rm deletes. There is no trash folder. echo the expanded command before rm when the line contains a glob or a variable. The Linux commands page covers rm in more detail.
Finding the command you are about to run
On this Zsh, whence, where, which, type, and command are builtins. /usr/bin/which also exists. The builtin and the external program do not answer the same question. whence -v reports aliases and functions. where lists every match. The external which does not know your shell functions.
whence -v zsh
where zsh
command -v zsh
type lscommand -v is the one to use in a script that might run under sh, because it is not a Zsh-only builtin. whence is the one to use while you are typing in Zsh.
functions lists functions. alias lists aliases. bindkey lists key bindings. setopt lists options that are on. man zsh opens the manual. man zshoptions, man zshzle, and man zshcompsys are the chapters you will actually use.
History
history and fc -l print recent commands. History expansion (!!, !$, ^old^new) re-runs text from the history list. That text can be a token you did not mean to type again.
Ctrl-R in the default emacs keymap is history-incremental-search-backward. Confirmed with bindkey '^R' under zsh -f after bindkey -e.
The minimal config sets:
HISTFILEnext to the other dotfiles ($ZDOTDIRor$HOME)HISTSIZEandSAVEHISTof 50000EXTENDED_HISTORYso the file stores a timestampSHARE_HISTORYso new commands are appended immediately and other interactive shells can import themHIST_IGNORE_ALL_DUPSso a repeated command keeps one entryHIST_IGNORE_SPACEso a command that starts with a space is not storedHIST_REDUCE_BLANKSandHIST_FCNTL_LOCKINTERACTIVE_COMMENTSso#works at the prompt
INC_APPEND_HISTORY appends immediately without importing other sessions. Use it instead of SHARE_HISTORY if you do not want one terminal to replay another’s commands. The shipped file uses SHARE_HISTORY only.
HIST_IGNORE_SPACE is not a security boundary. If you forget the space, the password, the presigned URL, or the curl line with a token is on disk in HISTFILE. Do not type secrets as arguments. Prefer a prompt, a file mode 0600, or a tool that reads from an agent or a secret store. Anyone who can read HISTFILE can read what you typed.
Completion
compinit loads the completion system. The minimal file calls it with -d so the dump file follows ZDOTDIR and a lab under /tmp does not write ~/.zcompdump.
Tab completes according to the context. Styles change how the list is presented. This pair is optional and is not in the minimal file:
zstyle ':completion:*' menu select
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}'The dump file is a cache. If completions are stale after a tool upgrade, remove that one file, the path you passed to -d, and start a new shell. compinit rebuilds it. Do not delete a whole cache directory to fix one dump.
compaudit reports directories on fpath that are group-writable or world-writable. compinit refuses to use them until you fix the permissions. compinit -u, -i, and -C skip or weaken that check. -C is faster because it skips the check. Do not make it the default. A completion file is shell code.
Extra definitions come from the command itself (kubectl completion zsh, docker completion zsh) or from a project such as zsh-completions. They are still code you source. Prefer the completion shipped with the CLI you installed, and add zsh-completions for commands that ship none.
Prompt and the line editor
PROMPT is the left prompt. RPROMPT is the right prompt. In the minimal file, %F{cyan}%~%f %# is the directory in cyan, then % for a normal user and # for root. Prompt escapes are documented in man zshmisc under prompt expansion.
bindkey -e selects the emacs keymap. bindkey -v selects vi. bindkey -l on this binary lists emacs, viins, vicmd, and others. Under emacs mode, Ctrl-A is beginning-of-line and Ctrl-E is end-of-line, confirmed with bindkey under zsh -f.
The line editor is ZLE. Widgets are functions bound to keys. zle -N registers one. You do not need a custom widget to be productive. Autosuggestions, later, work by wrapping widgets that already exist.
setopt CORRECT asks before it runs a command name it thinks is misspelled. It also asks when the name was right and the correction is wrong. It is not in the shipped configs.
Aliases and functions
An alias is text replacement before the command runs. A function is a command with arguments and a local scope.
alias ll='ls -la'
mkcd() {
mkdir -p -- "$1" && cd -- "$1"
}A global alias (alias -g) expands in the middle of a line, including after a pipe. That surprises people. Do not put one in a shared config.
Aliases are not expanded in the same way inside scripts, and they are invisible to programs that are not this Zsh. ll in a #!/bin/sh script is a missing command. Type the real command in scripts, in runbooks, and in anything an agent will copy.
Options that belong in a script, not in .zshrc
set -euo pipefail is accepted by this Zsh 5.9. -e is ERR_EXIT, -u is NO_UNSET, and pipefail is PIPE_FAIL. On 11 October 2026 a temporary interactive Zsh whose .zshrc contained only that line exited 1 when the next command was false. The word survived was not printed.
Use ERR_EXIT, NO_UNSET, and PIPE_FAIL in a script, near the top, after emulate -L zsh if you want a known option set. Do not put them in .zshrc. An interactive shell has to tolerate grep finding nothing.
Other options and what they actually do here:
AUTO_CD: a directory name as a command cds there. Convenient, and easy to cd by mistake.EXTENDED_GLOB: enables^,~,#, and the(#q...)qualifier form. Plain qualifiers such as(.)already worked on this 5.9 withEXTENDED_GLOBoff. A directoryb.txtwas skipped by*.txt(.). The filea.txtwas not.NULL_GLOB: an unmatched pattern becomes empty instead of an error. Dangerous next torm.NOMATCH: on by default (nonomatchwas off underzsh -f). An unmatched pattern printsno matches found. In a non-interactive script the shell then exits 1 and later lines do not run. Confirmed withzsh -f -cand a pattern that matched nothing.
Advanced features
Context: Zsh 5.9 on this Mac. Blocks that set EXTENDED_GLOB say so. Bash equivalents are named where the syntax collides.
Arrays
Zsh arrays are 1-indexed. This printed first=p and count=3.
typeset -a xs=(p q r)
print -r -- "first=${xs[1]} count=${#xs}"
typeset -A m=([region]=us-east-1)
print -r -- "map=${m[region]}"
print -r -- "upper=${(U)m[region]}"Bash arrays are 0-indexed, and ${xs[1]} there is the second element. typeset -A is Zsh. Bash 4 uses declare -A. Do not mix them in one file.
${(U)name} uppercases. That flag is Zsh parameter expansion, not Bash.
Globs you will use while cleaning a repo
**/*.txt listed a/b/file.txt under zsh -f without EXTENDED_GLOB.
print -l -- **/*.txt
print -l -- *.txt(.)**/* walks directories. (.) keeps regular files. Other qualifiers in the manual include / for directories and @ for symlinks. rm **/*.log is still rm. Print the expansion first.
Brace expansion is not a glob. {a,b}.txt becomes two words even if the files do not exist.
print -r -- {src,tests}/index.tsSubstitution and pipelines
Command substitution runs a command and keeps the output. Quote it unless you mean the shell to split words.
branch=$(git branch --show-current)
print -r -- "branch=$branch"Process substitution feeds a command’s output as if it were a file. Useful for a diff of two commands. It is not POSIX sh.
diff -u <(git show HEAD:package.json) <(git show origin/main:package.json)Pipelines run concurrently. The exit status is the last command unless PIPE_FAIL is set, in which case any failed stage fails the pipeline. Redirects: > truncates, >> appends, 2> is stderr. > on an existing file replaces it.
Functions, jobs, and traps
local limits a variable to the function. Without it, the assignment leaks into the interactive shell.
count_js() {
local n
n=$(print -l -- **/*.js(.N) | wc -l)
print -r -- "$n"
}(.N) is “regular files, and no error if none match”. The N is per-pattern NULL_GLOB. Use it when zero files is a valid answer. Do not use it on a rm line to hide a typo.
jobs lists background work. cmd & starts one. fg brings it back. Ctrl-Z suspends the foreground job.
TRAPINT() { print -ru2 -- 'interrupt'; return 0 }A trap runs your code on a signal. TRAPINT handles interrupt. An EXIT trap runs on shell exit. Keep traps short. A trap that starts more work makes a failing script harder to stop.
A small widget
You rarely need this. The pattern is: a function, zle -N, then bindkey.
fm-pwd() { zle -M "$(pwd)" }
zle -N fm-pwd
bindkey '^[p' fm-pwdzle -M prints a message below the prompt. Alt-P depends on the terminal sending ^[p. If it does nothing, the sequence is different. cat -v then the key shows what the terminal sends. This widget was not bound in the shipped configs.
Debugging a function
functions -t name traces that function. zsh -x script.zsh traces a file. Turn the trace off with functions +t name when you are done. Traces print expanded commands, which can include values you did not want in a scrollback.
Where Bash users get the test wrong
Inside [[ ]], both shells accept == for strings. Inside [ ], prefer a single =. Zsh and Bash also disagree on unquoted empty variables. Write "$var" unless you have a reason not to.
for loops and if are familiar. A Zsh script that uses print, arrays from index 1, or glob qualifiers is not a Bash script. Say so in the header.
Scripts and interactive configuration
Four different things get called “shell config”:
| Kind | File | Read when |
|---|---|---|
| Interactive | .zshrc | Interactive Zsh |
| Login environment | .zprofile | Login Zsh |
| Every Zsh, including scripts | .zshenv | Always, unless -f |
| A program | a file with a shebang | When you execute that file |
Shebangs:
#!/usr/bin/env zshfor a script that uses Zsh on purpose.#!/usr/bin/env bashfor Bash.#!/bin/shfor POSIX only. No arrays, no[[ ]], no process substitution, noprint.
chmod +x makes the file executable. ./script.zsh then uses the shebang. zsh script.zsh forces Zsh even if the shebang says something else, which hides a mistake. Test with the shebang path: ./script.zsh.
Arguments are "$@". Quote them. The exit code is the status of the last command, or an explicit exit. Temporary files: mktemp. Remove them in an EXIT trap if the script created them.
#!/usr/bin/env zsh
emulate -L zsh
setopt ERR_EXIT PIPE_FAIL NO_UNSET
f=$(mktemp)
trap 'rm -f "$f"' EXIT
print -r -- "args=$#" >"$f"zsh -n script.zsh parses without running. zsh -x script.zsh traces. The lab runs zsh -n on both example rc files and expects zsh_n=ok.
Plugin managers
A framework is a distribution of config, aliases, and a prompt. A plugin manager clones repositories and sources them. A plugin is a file of shell code. A theme is a prompt. A standalone tool is a binary in PATH that may also print a few lines of shell setup.
Plugins are not sandboxed. source runs as you.
| Approach | Setup | What you get | Cost | Use it when |
|---|---|---|---|---|
| Files you source yourself | Lowest | Exactly the files you read | You track clones | The list is three plugins or fewer |
| Oh My Zsh | Installer or a clone | A large plugin and theme catalog | Heavier startup, many aliases | You want git aliases and a theme today |
| Antidote 2.3.0 | Clone or brew install antidote | A plugins file and a static load script | You write the list yourself | You want an explicit, small set |
| Zinit | Upstream install edits .zshrc | Deferred loading (zinit ice wait) | More syntax to learn | You will use that deferral |
| Prezto | git clone --recursive into .zprezto | A full framework, minimum Zsh 4.3.11 | It replaces several rc files via symlinks | You want Prezto’s modules, not a plugin list |
Pick one. Do not install Oh My Zsh and Antidote together. Do not source a plugin that the manager also loads.
Antidote is the loader in the developer example. Release 2.3.0 was published on 7 August 2026. antidote load writes a static file (next to your plugins file, with a .zsh suffix) and sources that file. If the rebuild fails, the previous static file is left in place. That behavior is in the 2.3.0 antidote-load function, read for this page. The project also documents a hand-written if [[ ! file.zsh -nt file.txt ]] snippet. Upstream describes the extra speed as usually not worth it, and that snippet truncates the static file directly. The example uses antidote load instead. Do not also call antidote init. That command is a compatibility wrapper and is not the recommended path.
Install from the current instructions:
git clone --depth=1 https://github.com/mattmc3/antidote.git ${ZDOTDIR:-$HOME}/.antidoteOr brew install antidote. The Homebrew file on Apple Silicon is /opt/homebrew/opt/antidote/share/antidote/antidote.zsh. On Intel Homebrew it is under /usr/local/opt/antidote. This Mac did not have either file (antidote_file=absent). The example was parsed with zsh -n and was not sourced, so it did not clone plugins into this account.
pin: takes a full 40-character commit. min-age, added in 2.2.0 (27 July 2026), keeps bundles a number of days behind upstream. The Antidote release note says commit dates are attacker-controlled, so min-age is a cushion, not a guarantee. Pin a commit you have read if you want a freeze. This page does not invent a hash.
Oh My Zsh is the other reasonable start. The upstream install is a remote script. Read it, or clone the repository and source oh-my-zsh.sh yourself. Plugins go in a whitespace-separated array. Commas are not separators. The plugin list, generated from the wiki and edited 18 September 2026, is the index. Enable the few you will remember.
Zinit can defer plugins (zinit ice wait, then zinit light user/repo). The README’s easiest install is a remote script that writes into .zshrc. That is a poor fit if you already have a config. It was not run here.
Prezto is a framework. The manual install clones with --recursive and then symlinks runcoms into your home directory. Follow the current README rather than a copied ln loop. Those symlinks replace existing rc files if you are not careful.
Update plugins by reading the diff, not by updating all of them on Monday morning and hoping the prompt still works. One manager, one update command, one shell restart, then zsh -n on the rc file if you changed it.
Plugins and terminal tools
Versions below are what check-shell.sh printed on this Mac on 11 October 2026, or what the upstream page said when it was read the same day. Atuin is not installed here. Nothing in this section was installed for the article.
zsh-autosuggestions
A Zsh plugin. zsh-users/zsh-autosuggestions. As you type, a grey suggestion appears from history by default. Right arrow (forward-char) or End (end-of-line) accepts it when the cursor is at the end of the line. forward-word accepts one word. That is not Tab. Tab runs completion.
ZSH_AUTOSUGGEST_STRATEGY is an array. Built-in strategies are history, completion, and match_prev_cmd. The default path is history. match_prev_cmd does not behave as documented if HIST_IGNORE_ALL_DUPS is set, and the shipped configs set that option. Leave the strategy at history.
The completion strategy uses the completion system and costs more. Turn it on only if history suggestions are not enough. Suggestions are asynchronous on Zsh 5.0.8 and newer, which includes 5.9.
zsh-syntax-highlighting
A Zsh plugin. zsh-users/zsh-syntax-highlighting. Commands are colored while you type, so a missing program is visible before Enter.
Source it at the end of the plugin list. The project says it must be sourced at the end of .zshrc because it hooks the line editor. On Zsh 5.8 and older it wraps widgets, and widgets created afterward do not refresh highlighting. On Zsh newer than 5.8, including 5.9 and 5.9.2, it registers a zle-line-pre-redraw hook, and that hook should be registered after other hooks that change the buffer.
fzf --zsh on fzf 0.74.4 does not register that hook. The developer file therefore loads fzf after Antidote. Highlighting is still the last plugin inside the Antidote list.
fast-syntax-highlighting
A different plugin. zdharma-continuum/fast-syntax-highlighting. More highlighters, and Antidote’s own examples show it with kind:defer. It solves the same problem as zsh-syntax-highlighting. Load one. The example loads zsh-syntax-highlighting because the load-order contract is documented by the zsh-users project and the hook behavior matches Zsh 5.9. Use fast-syntax-highlighting if you have already chosen it and you are not also loading the other one.
zsh-completions
A Zsh plugin of completion functions. zsh-users/zsh-completions. Antidote line: zsh-users/zsh-completions kind:fpath path:src. kind:fpath only adds the src directory to fpath. It does not execute every file. compinit autoloads what it needs.
Antidote’s default fpath rule is append (2.3.0 source: _ANTIDOTE_FPATH_RULE=append). Appended directories are searched after the ones already on fpath, so a completion that already exists is not replaced. That is what you want. fpath-rule:prepend would let this repository override a completion shipped with the system. Do that only for a command you know is missing or wrong.
The example calls fm_zsh_compinit in a post: annotation on that line, so compinit runs after fpath is updated and before the later plugins. post: is emitted as a function call in the static file. The function has to exist before antidote load. It is defined earlier in developer.zshrc.
fzf
A program, with a Zsh integration. junegunn/fzf. This Mac has 0.74.4 from Homebrew. fzf --zsh prints the shell code. The integration binds Ctrl-T to a file widget, Alt-C to a directory widget, Ctrl-R to history, and Tab to fzf-completion. Tab still uses ordinary completion unless the word ends with the trigger, which defaults to **.
if command -v fzf >/dev/null 2>&1; then
source <(fzf --zsh)
fiThose bindings were read from fzf --zsh on this machine. The widgets were not clicked in a terminal for this page. Ctrl-R will be taken by Atuin if you enable Atuin later. Decide which program owns that key.
zoxide
A program. ajeetdsouza/zoxide. This Mac has 0.10.0. It remembers directories you visit and jumps with z. cd still works.
if command -v zoxide >/dev/null 2>&1; then
eval "$(zoxide init zsh)"
fizoxide init zsh can take an fzf flag in current upstream docs if you want an interactive picker. Add that only after plain z is familiar. Migrating from autojump or z (the older tool) is a database import documented by zoxide. Do not run both jumpers.
Atuin
A program. atuinsh/atuin. Not installed here (atuin=absent). The docs were read, the binary was not run.
Atuin stores history in its own database with directory, exit status, and duration, and it can sync that database. The Zsh line in the current install guide is eval "$(atuin init zsh)". It rebinds Ctrl-R, and up-arrow is configurable. Native HISTFILE remains unless you stop writing it. You then have two histories.
The README’s quickstart is a remote install script, then atuin register, which signs you up for Atuin’s hosted sync. Prefer your package manager (brew install atuin, or the distro package) and read the sync guide before you register. The README says you can skip sync and keep history local. Do that if the history should not leave the machine.
Sync is described as end-to-end encrypted. That does not make the laptop safe, and it does not make the host that stores ciphertext irrelevant. The shell sees the command in the clear before anything is encrypted. HIST_IGNORE_SPACE still matters. A token in a command line is still a token on that machine.
The developer file leaves Atuin commented. Uncomment it only if it should own Ctrl-R, and then accept that fzf’s history widget is no longer on that key.
Starship
A program. starship/starship. This Mac has 1.26.0, built 28 June 2026. It is not a Zsh plugin and not a terminal theme.
if command -v starship >/dev/null 2>&1; then
eval "$(starship init zsh)"
fiConfiguration is ~/.config/starship.toml. Git status in the prompt is useful and it is also the usual reason a prompt is slow in a large repository. Disable the modules you do not read. The minimal file’s PROMPT remains for machines without Starship.
direnv
A program. direnv/direnv. This Mac has 2.38.2. The hook runs on directory entry and, after you have allowed a file, evaluates the environment that .envrc produces.
.envrc is code. direnv allow is the trust decision. Do not allow a file you have not read. Do not allow every repository an agent clones. The hook is commented out in the developer file. Uncomment it when you want the integration, then allow projects one by one.
eval "$(direnv hook zsh)" is the upstream line. The hook evaluates direnv’s export. That is still your shell, not a sandbox.
Oh My Zsh plugins worth knowing about
These names are on the wiki index as of 18 September 2026. They are useful inside Oh My Zsh. They are not a list to paste into plugins=(...) all at once. If you use Antidote, prefer each tool’s own completion so you do not also take the aliases.
| Plugin | What it adds | Watch for |
|---|---|---|
git | Many git aliases and a few functions | An alias can hide a flag you meant to type. whence -v gco before you depend on it. |
docker | Completion and aliases | Aliases that shadow docker subcommands. Docker 29.8.2 on this Mac already provides docker completion zsh. |
kubectl | Completion and short aliases | kubectl completion zsh on client 1.36.1 printed a #compdef script. You do not need both. |
aws | Completion for AWS CLI v2, plus profile helpers | The wiki says CLI v1 is not supported. Do not use the plugin to store keys. |
brew | Aliases for Homebrew | macOS and Linuxbrew only. |
npm | Completion and aliases | Can surprise you if the alias is not the npm you meant. |
bun | Completion for Bun | Bun 1.4.0 is on this Mac. Completion still belongs to one provider. |
python | Aliases for Python commands | python versus python3 differs by OS. |
composer | Completion, aliases, and Composer global bins on PATH | Composer 2.10.3’s own completion help says only bash is supported. The plugin is the Zsh path if you already use Oh My Zsh. |
sudo | Escape twice prefixes the current or previous command with sudo | Easy to elevate the wrong line. |
command-not-found | Suggests a package | Needs the distro’s command-not-found handler. It is not a solver. |
colored-man-pages | Color in man pages | Harmless. Still optional. |
extract | An extract function for several archive types | It runs the matching unpacker. Look at the file name first. |
Powerlevel10k
romkatv/powerlevel10k is a Zsh prompt theme. The README, checked for this page, says the project has very limited support, no new features are in the works, most bugs will go unfixed, and help requests will be ignored. The maintainer has also said it can keep working without new features. That is a reason to leave an existing setup alone. It is not a reason to choose it for a new one.
Starship is the prompt in the developer example. A one-line PROMPT is the prompt in the minimal example. Oh My Zsh themes are a third path if you already chose Oh My Zsh. Do not run Powerlevel10k and Starship together. Both set the prompt.
Which set to install
- Minimal. The minimal file. No plugin manager. Completion, history, a one-line prompt.
- Daily. Antidote,
zsh-completions,zsh-autosuggestions,zsh-history-substring-search,zsh-syntax-highlighting, fzf, zoxide, Starship. - macOS. The daily set. Add the
brewplugin only if you are inside Oh My Zsh and you want those aliases. The system Zsh may be older than Homebrew Zsh. Know whichcommand -v zshprints. - AWS and DevOps. Daily set, plus the CLI’s own completion for
aws,kubectl, anddocker. Not the full Oh My Zsh alias pack on top. - Advanced. Zinit, if you will use deferred loading. Still one highlighter. Still one prompt.
- Privacy or offline. No Atuin sync. Leave Atuin commented, or run it without
atuin register. Do not put tokens inHISTFILE. Do notdirenv allowa repo you have not read.
Autosuggestions are not completion. Native history is not Atuin. A plugin is not a binary in /opt/homebrew/bin. Oh My Zsh is not Antidote. Starship is not Ghostty.
Two configurations
Both files live in the repo. zsh -n accepts them. The lab sources only the minimal file, and only inside a temporary ZDOTDIR.
Minimal
Context: Zsh 5.9. No plugins. Copy to ${ZDOTDIR:-$HOME}/.zshrc.
# Minimal interactive Zsh. Zsh 5.9 on the lab Mac. No plugins.
# Copy to ${ZDOTDIR:-$HOME}/.zshrc. Do not copy it into Bash scripts.
# History stays next to the other dotfiles. A leading space skips one entry.
HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history
HISTSIZE=50000
SAVEHIST=50000
setopt EXTENDED_HISTORY
setopt SHARE_HISTORY
setopt HIST_IGNORE_ALL_DUPS
setopt HIST_IGNORE_SPACE
setopt HIST_REDUCE_BLANKS
setopt HIST_FCNTL_LOCK
setopt INTERACTIVE_COMMENTS
# Completion dump follows ZDOTDIR so a throwaway lab does not write into $HOME.
autoload -Uz compinit
compinit -d "${ZDOTDIR:-$HOME}/.zcompdump"
# %~ is the directory, shortened with ~. %# is % for a user and # for root.
PROMPT='%F{cyan}%~%f %# 'Published copy: /examples/architecture-blog-2026/mastering-developer-tools/mastering-zsh/minimal.zshrc.
Developer
Context: Zsh 5.9 or newer, Antidote 2.3.0. Copy developer.zshrc to ${ZDOTDIR:-$HOME}/.zshrc. Copy the plugins file to ${ZDOTDIR:-$HOME}/.zsh_plugins.txt. Install Antidote first. The command -v guards skip fzf, zoxide, and Starship when they are absent. Atuin and direnv stay commented.
Plugins file:
# Antidote 2.3.0 plugins file. One bundle per line. Comments start with #.
# Copy to ${ZDOTDIR:-$HOME}/.zsh_plugins.txt
# fm_zsh_compinit and fm_zsh_history_keys are defined in developer.zshrc
# before this file is sourced. Do not add a second syntax highlighter.
zsh-users/zsh-completions kind:fpath path:src post:fm_zsh_compinit
zsh-users/zsh-autosuggestions
zsh-users/zsh-history-substring-search post:fm_zsh_history_keys
zsh-users/zsh-syntax-highlightingzsh-history-substring-search wants Up and Down to walk history that contains the buffer. Its README tells you to bind the keys after the plugin is sourced, and it also says to load syntax highlighting first. The highlighter’s README says the highlighter has to be last. This file follows the highlighter: history search, then its keys, then highlighting. If Up and Down do nothing, the arrow sequence from cat -v is not ^[[A / ^[[B. Replace those two strings. Do not load a second highlighter to “fix” the keys.
developer.zshrc:
# Developer Zsh. Zsh 5.9 or newer. Antidote 2.3.0 static bundle.
# Copy to ${ZDOTDIR:-$HOME}/.zshrc and copy developer.zsh_plugins.txt to
# ${ZDOTDIR:-$HOME}/.zsh_plugins.txt. Do not also start Oh My Zsh.
HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history
HISTSIZE=50000
SAVEHIST=50000
setopt EXTENDED_HISTORY
setopt SHARE_HISTORY
setopt HIST_IGNORE_ALL_DUPS
setopt HIST_IGNORE_SPACE
setopt HIST_REDUCE_BLANKS
setopt HIST_FCNTL_LOCK
setopt INTERACTIVE_COMMENTS
# Used only when Starship is absent. Starship replaces PROMPT below.
PROMPT='%F{cyan}%~%f %# '
# post:fm_zsh_compinit runs after zsh-completions is added to fpath.
fm_zsh_compinit() {
autoload -Uz compinit
compinit -d "${ZDOTDIR:-$HOME}/.zcompdump"
}
# post:fm_zsh_history_keys runs after history-substring-search is sourced
# and before zsh-syntax-highlighting. Arrow sequences differ by terminal.
fm_zsh_history_keys() {
bindkey '^[[A' history-substring-search-up
bindkey '^[[B' history-substring-search-down
bindkey -M emacs '^P' history-substring-search-up
bindkey -M emacs '^N' history-substring-search-down
}
# Stat known paths. `brew --prefix` is a process, so it is not on this path.
antidote_zsh=
for candidate in \
"${ZDOTDIR:-$HOME}/.antidote/antidote.zsh" \
/opt/homebrew/opt/antidote/share/antidote/antidote.zsh \
/usr/local/opt/antidote/share/antidote/antidote.zsh
do
if [[ -r $candidate ]]; then
antidote_zsh=$candidate
break
fi
done
plugins_txt=${ZDOTDIR:-$HOME}/.zsh_plugins.txt
if [[ -n $antidote_zsh && -r $plugins_txt ]]; then
# antidote load writes a static file and sources it. On a failed rebuild
# it keeps the previous static file. Do not also call antidote init.
source "$antidote_zsh"
antidote load "$plugins_txt"
else
fm_zsh_compinit
fi
# fzf is a program. `fzf --zsh` prints the integration; it is not a plugin repo.
if command -v fzf >/dev/null 2>&1; then
source <(fzf --zsh)
fi
if command -v zoxide >/dev/null 2>&1; then
eval "$(zoxide init zsh)"
fi
if command -v starship >/dev/null 2>&1; then
eval "$(starship init zsh)"
fi
# Atuin also binds Ctrl-R. Uncomment only if Atuin should own history search.
# if command -v atuin >/dev/null 2>&1; then
# eval "$(atuin init zsh)"
# fi
# direnv runs an allowed .envrc on directory entry. Uncomment after you
# have read https://direnv.net/ and you will not allow a file you have not read.
# if command -v direnv >/dev/null 2>&1; then
# eval "$(direnv hook zsh)"
# fiPublished copies: /examples/architecture-blog-2026/mastering-developer-tools/mastering-zsh/developer.zshrc and /examples/architecture-blog-2026/mastering-developer-tools/mastering-zsh/developer.zsh_plugins.txt.
The first interactive start after you add the plugins file will clone those four repositories into Antidote’s cache (~/Library/Caches/antidote on macOS, unless ANTIDOTE_HOME is set). That needs a network. Later starts source the static file.
Developer workflows
These are shell integrations, not tutorials for the tools. Command references for the tools themselves are the other parts of this series: Git, AWS CLI, Docker, and Kubernetes.
PATH
The first matching executable wins. where node and whence -av node show every copy. On this Mac, node is Homebrew, bun is ~/.bun/bin/bun (1.4.0), and php is Homebrew 8.5.11. An alias named node or a function named aws hides the real binary. Check with whence -v before you blame the install.
Do not prepend a project node_modules/.bin in .zshenv. That directory is different in every repo. Use npx, a package script, or direnv after you have read the .envrc.
Git
The shell’s job is completion and history, not a second set of git aliases, unless you already like Oh My Zsh’s git plugin. git completion ships with Git. Confirm with whence -v git.
Switching repositories is cd, pushd, or z from zoxide. Finding a previous git command is Ctrl-R or, with the history-substring plugin, Up after you type git. Reviewing what an agent changed is still git status and git diff, covered on the Git page.
Node, Bun, PHP
Node and Bun both install a node only if you asked them to. Bun on this machine did not replace command -v node. Keep it that way unless you mean to. bun --version and node --version are the check.
Composer 2.10.3’s composer completion --help says bash is the supported shell. Do not pass zsh to that command and expect a Zsh function. Use the Oh My Zsh composer plugin if you are already on Oh My Zsh, or a bash completion loaded through bashcompinit if you have read that script. Do not alias php.
Python virtual environments belong to the project. source .venv/bin/activate is explicit. direnv can do it from an .envrc you have allowed. Activating a venv from .zshrc applies one environment to every directory, which is the wrong scope.
Docker and kubectl
On this Mac, docker completion zsh (Docker 29.8.2) exited 0 and wrote a completion script, which was discarded, not sourced. kubectl completion zsh (client 1.36.1) printed a #compdef header.
Put the output on fpath or source it after compinit, using the command the current CLI documents. Do not also enable the Oh My Zsh aliases unless you want dcup and similar names. An alias that redefines kubectl to add a default namespace will surprise a script that expected the binary.
AWS CLI
AWS CLI 2.37.12 is on this Mac, and aws_completer is on PATH. The usual Zsh wiring uses bash completion compatibility because the completer is written that way:
autoload -Uz bashcompinit && bashcompinit
complete -C aws_completer awsThat block is not in the shipped rc files. Add it if Tab on aws does nothing. The Oh My Zsh aws plugin is the alternative when Oh My Zsh is your loader. It does not support CLI v1.
Do not export AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY from .zshrc. Use a profile, Identity Center, or a role. AWS_PROFILE for one shell is export AWS_PROFILE=name in that shell, or an .envrc you have read. aws sts get-caller-identity before a command that can spend money is the check on the AWS CLI page.
A missing program is command -v name and then the package manager. command-not-found is a hint, not an installer you should trust blindly.
Startup time
Measure before you delete plugins. A single /usr/bin/time -p zsh -f -c exit on this Mac printed real 0.00, because that timer reports hundredths of a second and the process was faster than that. The lab uses Python time.perf_counter around 20 runs instead. Warm the completion dump once, then record the median. Compare the same machine, the same ZDOTDIR, before and after one change.
zprof attributes time inside one startup. Put this at the top of the file under test and the report at the bottom. Remove both when you are finished. Left in place, zprof prints a table on every new terminal.
zmodload zsh/zprof
# ...the rest of the file...
zprofWhat usually shows up:
- A
sourceof a framework you forgot was there. compinitrebuilding.zcompdumpbecause the dump is missing or older thanfpath.antidote bundlerunning on every start because the static file is not newer than the plugins list.antidote loadshould skip that once the static file is current.- A prompt that runs
git statusin a huge work tree. That cost is per prompt, not only at startup. Starship and Powerlevel10k both do this if the git module is on. brew --prefix,nvm.sh, and other processes launched from.zshrc. The developer file checks three antidote paths with[[ -r ]]and does not callbrew --prefix.
Lazy loading helps when a plugin is expensive and you rarely need it before the first prompt. It hurts when the first command pays the cost anyway and the failure is harder to see. Antidote’s kind:defer needs zsh-defer. The example does not defer the highlighter or the suggestions, so the order stays obvious.
Do not pass -C to compinit to win a millisecond. Fix the directory compaudit names, or accept the check.
The 48.2 ms figure includes compinit and the history options. It does not include Antidote, fzf, zoxide, or Starship. Those were not sourced in the timing run. There is no target number in this page.
Security
source ./file runs that file as your user. A plugin from GitHub does too. A theme does too. There is no container around .zshrc.
Before you trust a repository: read the commit you are about to source, prefer a tagged release or a pinned full SHA, and prefer the distro package or Homebrew when one exists. The Oh My Zsh, Zinit, and Atuin quickstarts are remote scripts. A remote script can edit your rc files. Read it, or clone the repository yourself.
eval "$(some-tool init zsh)" runs whatever the tool prints. That is normal for Starship, zoxide, direnv, and Atuin, and it is only as safe as that binary. Do not eval a string you copied from a chat.
History: see HIST_IGNORE_SPACE above. It misses anything you did not prefix with a space. Do not put tokens in the command line.
Unquoted variables and unquoted globs expand. rm $dir when dir is empty, or rm * in the wrong directory, deletes more than you think. Quote. echo the line first.
sudo runs the command as root. It is not a retry for “command not found” or “permission denied” until you have read the path. The Oh My Zsh sudo plugin makes that retry a double tap of Escape.
direnv allow is an approval to execute .envrc. Withdrawing approval is direnv deny. Do not allow a directory an agent just cloned until you have opened the file.
PATH order is the other shadowing problem. A writable directory at the front of PATH means a program named ls or git in that directory runs instead of the system one. Prepend a directory only when you mean to override what follows.
Updating every plugin at once applies code you have not read. Update one, open a new terminal, and see if completion and the prompt still work.
Troubleshooting
| Symptom | Check | Fix | Verify |
|---|---|---|---|
| Terminal window opens and closes | zsh -n ~/.zshrc and zsh -f | The parse error is the line zsh -n names. zsh -f gives you a shell so you can edit. | A new terminal stays open and echo $ZSH_VERSION prints a version. |
Bash starts, or ZSH_VERSION is empty | ps -p $$ -o comm= and the terminal profile | The profile or SSH command starts bash. chsh does not rewrite a profile that names bash. | echo $ZSH_VERSION in that profile. |
PATH is wrong | print -r -- $PATH and whence -a name | Edit .zshenv or .zprofile, not a random plugin. Later entries do not win. | command -v name is the binary you expect. |
| Command not found | command -v name and type name | Install it, or remove an alias that replaced it. | whence -v name says the path or the function you wanted. |
| Plugin did not load | The static file and antidote load output | The plugins file has to be at ${ZDOTDIR:-$HOME}/.zsh_plugins.txt. A failed clone leaves the previous static file. | whence -v a function the plugin defines, after a new shell. |
| Tab does nothing useful | whence -v compinit and echo $fpath | compinit never ran, or it ran before the completion directory was on fpath. | Tab on git offers subcommands. |
| Completions are stale | The dump path passed to compinit -d | Delete that dump file only. The next compinit rebuilds it. | Tab shows the new subcommand. |
| Keys do nothing | bindkey '^R' and cat -v plus the key | The sequence does not match. Arrow codes differ. Another plugin bound the key later. | bindkey shows the widget you chose. |
| No grey suggestion | History is empty, or the plugin is not sourced | Type a command, run it, then start the same prefix. HIST_IGNORE_SPACE hides lines that began with a space. | A suggestion appears and End accepts it. |
| Colors never update | Load order | Highlighting is not last among plugins, or a second highlighter is also loaded. | A missing command is colored before Enter. |
| Prompt boxes or missing glyphs | The terminal font | The font has no Nerd Font icons. Starship’s symbols need a font that contains them, or set the preset to plain text. | The prompt uses ASCII, or the font includes the icons. |
| Startup is slow | zprof, then remove it | One source, a prompt git status, or a dump rebuild. See the previous section. | A second timed run on the same machine is lower after one change, and Tab still works. |
| Two aliases, one name | whence -v name and alias name | The later alias or source wins. Remove one. | alias lists a single definition. |
no matches found | The glob, under zsh -f | Quote it, or add (N) on that pattern if zero matches is acceptable. Do not setopt NULL_GLOB globally next to rm. | print -r -- the pattern shows the files you meant. |
| History is empty next week | echo $HISTFILE and ls -l that file | HISTFILE is unset, or RCS was off at exit, or the file is not writable. | A new shell shows yesterday’s history. |
| A plugin broke another | zsh -f, then source half the list | Two highlighters, two Ctrl-R owners, or Oh My Zsh plus Antidote. | One loader, one highlighter, one history key. |
| SSH looks different | Login versus interactive | SSH often starts a login shell. A setting only in .zshrc is present. A setting only in .zprofile is present for SSH and missing in a nested zsh. | echo $ZSH_VERSION and whence -v the tool, in both places. |
Exercises
- See the shell you have. Run
zsh --version,echo $ZSH_VERSION,echo $SHELL, andps -p $$ -o comm=. Pass:ZSH_VERSIONmatcheszsh --version. If you run the same echoes underbash,ZSH_VERSIONis empty. - Minimal rc in a temporary directory.
mkdira directory under/tmp, copyminimal.zshrcto$that/.zshrc, and startZDOTDIR=that zsh -ic 'alias; echo $HISTFILE; exit'. Pass:HISTFILEis inside that directory, not your home directory. Exit the shell. Delete the directory. - History. In that temporary shell, run
echo hello, thenecho secretwith a leading space, thenhistory. Pass:hellois listed andsecretis not. Confirm the history file does not containsecret. - Completion. Start the minimal shell and press Tab after
git. Pass: subcommands appear. If they do not,whence -v compinitshould show the function, and you are not inzsh -f. - One plugin, after you choose a loader. Install Antidote or clone one plugin and
sourceit. Do not install a second manager. Pass: a new shell defines a function from that plugin (whence -v) andzsh -non your rc file prints nothing. - fzf and zoxide. Install them with your package manager if
command -vmisses them. Add the guarded lines from the developer file. Pass:command -v fzfandcommand -v zoxideprint paths.zjumps to a directory you have visited. Ctrl-T is fzf’s file widget only if you sourcedfzf --zsh. - Time a change. Run the lab script. Change one thing in a copy of the minimal file under
/tmp(for example addzprof). Time 20 warm starts again. Pass: you can say which number moved, and you removedzprofafterward. Do not publish someone else’s number as your own. - Put the files in git. Keep
.zshrcand.zsh_plugins.txtin a dotfiles repository. Do not commitHISTFILE,.zcompdump, or a file that contains a token. Pass: a second machine can copy the rc files, install the same binaries, and open a shell. Completions for tools that are not installed stay behindcommand -vand do not abort startup.
Checklist
echo $ZSH_VERSIONmatches the binary you meant.$SHELLis not that test.- Interactive config is in
.zshrc..zshenvis short. - One plugin loader. One syntax highlighter. One prompt.
compinitruns after extra completion directories are onfpath, and without-C.- Optional programs are behind
command -vor[[ -r ]]. - You timed one change on your machine before you called the shell slow.
- The same rc files work on the next machine without a hardcoded secret.
HISTFILEis not a secret store..envrcis not allowed unread.- Plugin updates are diffs you looked at.
What this post does not cover
Zsh 5.9.2 was not installed here, so behavior that exists only in that release was not run. Fish, Nushell, and PowerShell are out of scope. So is a catalog of Oh My Zsh themes, a Powerlevel10k p10k configure walkthrough, and operating an Atuin server. Windows Terminal profile JSON is only mentioned, not listed. Completion for every AWS subcommand is the AWS CLI’s job. The AI agent tools page covers the coding-agent CLIs. This page covers the shell those agents type into.
What to do this week
- Run the lab script and keep the output. Your medians will differ.
- If
.zshrcis a stack of installers, move interactive setup into one file and start from the minimal example in a temporaryZDOTDIRbefore you replace the real file. - Choose Antidote or Oh My Zsh. Remove the other if both are installed.
- Add fzf and zoxide only after
command -vshows they are missing and you want them. - Leave
ERR_EXITout of the interactive file. Put it in scripts you execute on purpose.
Further reading
- Zsh manual
- Startup files
- Antidote
- Oh My Zsh plugins
- zsh-autosuggestions
- zsh-syntax-highlighting
- fzf
- zoxide
- Starship
- Atuin
- direnv
- Powerlevel10k
Series: Git, Linux commands, AWS CLI, Docker, Kubernetes, Bedrock CLIs, AI agent tools.
Contact us or see AWS DevOps consulting if you want this shell setup written down for a team that shares laptops and CI images.
Frequently asked questions
When should you not put ERR_EXIT or pipefail in .zshrc?
When should you not run direnv allow?
Should Oh My Zsh and Antidote both be installed?
Does echo $SHELL tell you which shell is running?
Is Powerlevel10k the prompt to install on a new setup?
Should compinit -C be used to make startup faster?

AWS Cloud Architect & AI Expert
AWS-certified cloud architect and AI expert with deep expertise in cloud migrations, cost optimization, and generative AI on AWS.




