Cheat Sheet

zsh Commands

zsh has been the default shell on macOS since Catalina and is a drop-in upgrade for bash on Linux. This sheet covers history search, aliases, completion, globbing, arrays, PATH, the startup files, plugins, and fixes for the errors it throws most.

Last updated September 11, 2026

CommandWhat it does
Search history as you type (press again for older matches)
Show the last 16 commands
Show the whole history
Search history for a command
Rerun the last command ( reruns it as root)
Last argument of the previous command
All arguments of the previous command
Rerun the most recent command starting with ssh
Rerun history entry 42
Insert the previous command's last argument (repeat to go older)
after typing Only steps through matching commands if you bind prefix search (below)
List history with date and time (needs )
Check history is being saved at all; empty means it dies with the session
Wipe saved history (zsh has no )

History size and behaviour go in :

HISTSIZE=50000
SAVEHIST=50000
setopt SHARE_HISTORY          # share history across open terminals
setopt HIST_IGNORE_ALL_DUPS   # keep one copy of a repeated command
setopt HIST_IGNORE_SPACE      # commands starting with a space are not saved
setopt EXTENDED_HISTORY       # record a timestamp with each command

# Up/Down search commands starting with what you typed
bindkey '^[[A' history-beginning-search-backward
bindkey '^[[B' history-beginning-search-forward

means the file has bad bytes in it. Salvage what is readable:

mv ~/.zsh_history ~/.zsh_history_bad
strings ~/.zsh_history_bad > ~/.zsh_history
fc -R ~/.zsh_history

Keyboard Shortcuts

Emacs-style bindings, the default. See the next section for vi mode.

KeyAction
/ Start / end of line
/ Forward / back one word
Delete the word before the cursor
Delete the whole line
Delete from cursor to end of line
Paste what Ctrl+W/U/K deleted
Clear the screen
Cancel the current line or interrupt the running command
Log out on an empty line; delete char otherwise
Suspend the foreground job ( resumes)
Complete; press again to cycle candidates
Edit the current line in $EDITOR (after )

Aliases

CommandWhat it does
Define an alias for this session
List every defined alias
Show one alias's definition
Remove an alias
Run the real command, ignoring the alias ( also works)
Global alias, expands anywhere in the line:
Suffix alias: typing opens it in less
Typing opens it in VS Code

Aliases live for one session unless you put them in :

echo "alias ll='ls -lah'" >> ~/.zshrc
source ~/.zshrc

An alias cannot take arguments; anything you type is appended to the end. When you need the argument in the middle, write a function instead:

mkcd() { mkdir -p "$1" && cd "$1" }
gclone() { git clone "https://github.com/$1.git" && cd "${1##*/}" }

Completion

zsh's completion system knows flags, hostnames, git branches, and PIDs, but only after you turn it on.

CommandWhat it does
Enable completion (put in )
Complete; a second Tab lists candidates
(with menu select)Arrow-key menu over the candidates
Turn on the arrow-key menu
Case-insensitive completion
Rebuild the cache when completions go stale
Fix the "compinit: insecure directories" warning

Keybindings and Vi Mode

CommandWhat it does
Emacs mode (the default)
Vi mode: Esc for normal mode, back to insert
List current bindings
Restore Ctrl+R in vi mode (not bound there by default)
Make Esc register instantly in vi mode (in )
See what a specific key runs

In vi normal mode: / step through history, searches it, opens the line in your editor.

Globbing

zsh globbing does what you would otherwise need for. Recursive works out of the box.

PatternWhat it matches
All .js files, recursively
Regular files only
Directories only
Symlinks only
The most recently modified file
The five most recently modified
Files larger than 10 MB
All .txt except notes.txt (needs extendedglob)
Everything except .md files (needs extendedglob)
Files, and no error when nothing matches
setopt extendedglob   # enables ^negation and the ~exclusion operator

For heavier searches (by mtime, with -exec), see the find cheat sheet.

renames in bulk using the same patterns, with for a dry run:

autoload -Uz zmv
zmv -n '(*).jpeg' '$1.jpg'      # drop -n to actually rename
zmv -n '(**/)(*).log' '$1$2.txt'
zmv -nW '*.txt' 'backup_*.txt'  # -W uses plain wildcards instead of groups

Arrays

zsh arrays start at 1, not 0, and a bare gives every element rather than the first. Both trip up pasted bash code.

CommandWhat it does
Define an array
First element (bash starts at )
Last element
or Every element
Number of elements
Append an element
Slice, elements two to three
Print one element per line
Join the array with commas
Split a string on commas into an array
Unique elements only
Sorted
Associative array (hash)
/ All keys / all values
Force word splitting on a plain string

Environment and PATH

zsh ties the array to , so you can edit it like a list instead of pasting colons.

CommandWhat it does
Set a variable and pass it to child processes
Shell variable only, not visible to commands you run
Set it for one command and nothing else
Remove it
/ List every exported variable / print one
Show PATH one entry per line
Append a directory to PATH
Prepend so your copy wins over the system one
Drop duplicate PATH entries
Show every match on PATH, first one wins
Say whether a name is an alias, function, builtin or binary

PATH belongs in so it is set once per login instead of growing in every nested shell:

# ~/.zprofile
typeset -U path
path=(/opt/homebrew/bin /opt/homebrew/sbin $path ~/bin)
export PATH

Startup Files

Read in this order. macOS Terminal and iTerm open login shells, so and both run; most Linux terminals open non-login shells, so only runs.

FileWhen it runsPut here
Every zsh, even scriptsAlmost nothing; keep it minimal
Login shells, oncePATH exports,
Every interactive shellAliases, functions, prompt, keybindings, completion
Login shells, after zshrcRarely used
When a login shell exitsCleanup, rarely used

After editing any of them: (or open a new tab).

Prompt

CommandWhat it does
user@host, current directory, then % (# as root)
Just the last path component
Colour a segment ( on, off)
Right-side prompt showing the time
Enable the built-in theme system
List built-in themes ( tries one)

Common escapes: user, host, directory with , % or #, time, last exit code. Most people skip hand-rolling and install powerlevel10k or starship, which come with git status, exit codes, and language versions built in.

Oh My Zsh

A config framework: themes, completion tweaks, and plugins in one install. Everything lives in after it is set up.

CommandWhat it does
Enable plugins (edit the line in )
Pick a theme
Update Oh My Zsh
List available plugins
Apply config changes to the current session

The git plugin is why most people install it. The aliases you will actually use:

AliasExpands to
/ /
current branch
/ /

Run to see the full list (150+).

Plugins

The two everyone installs are autosuggestions and syntax highlighting. Neither needs a framework; they are repos you source from .

PluginWhat it does
Greys out the rest of a command from history; Right arrow accepts it
Colours the line as you type, red for an unknown command; source it last
Up/Down walk only the history entries matching what you typed
fuzzy history, fuzzy file picker, fuzzy cd
(or the plugin) jumps to the directory matching proj you use most
Tells you when an alias already exists for what you just typed
git clone https://github.com/zsh-users/zsh-autosuggestions ~/.zsh/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-syntax-highlighting ~/.zsh/zsh-syntax-highlighting

# in ~/.zshrc, syntax highlighting must be sourced last
source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
source ~/.zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

With Oh My Zsh, clone into instead and add the names to the line. If startup gets slow, a manager that lazy-loads (zinit, antidote, zim) is the usual next step, or drop the framework and keep the lines.

Command Not Found Fixes

means the binary is not on your PATH or is not a binary at all. The pattern is the same for brew, pip, npm, nvm, docker, and code.

SymptomFix
(Apple Silicon)Add to , open a new tab
Any tool you know is installed, then add its directory: in
Installed seconds ago, still not found so zsh refreshes its command cache
nvm is a function; its init lines belong in , run the installer again or copy them over
Worked in bash, gone in zshMove PATH exports from / into
In VS Code: Cmd+Shift+P, "Shell Command: Install 'code' command in PATH"

Permission Denied

is about the file, not about zsh. The shell found it and the kernel refused to run it.

SymptomFix
, then run it again
Still denied after Check ownership with , or run to bypass the exec bit
Denied on a USB stick or network shareThe volume is mounted ; copy the file to your home directory first
writing to Install somewhere you own, or prefix the command with
on macOSGive your terminal Full Disk Access in System Settings, Privacy & Security
in a scriptThe script wants input but has none; feed it with or run it interactively
Denied only under drops your aliases, functions and PATH; call the binary by full path

Install and Switch Shells

CommandWhat it does
Check the version you are on
Show your login shell
Show the shell you are actually in right now
Newer zsh than the one macOS ships
Debian, Ubuntu
Fedora, RHEL
Shells accepts; a brew zsh must be listed here first
Make zsh your login shell (takes effect on next login)
Switch back to bash
Reload zsh in place after editing config
Start with no config at all, to prove your config is the problem
Trace a script line by line

On Windows there is no native zsh. Install it inside WSL (, then ) and set it as the default shell in the Linux distro, not in PowerShell.

One-Liners and Substitution

CommandWhat it does
Run the second only if the first succeeds
Run both regardless
Run the second only if the first fails
Capture a command's output in a variable
Substitute output inline
Arithmetic
Loop over files on one line

zsh vs bash

Typing commands feels identical, since , , and are programs, not shell features. What differs is quoting, arrays and globbing. For bash syntax itself, see the bash cheat sheet.

Differencezshbash
Unquoted with spacesStays one wordSplits into words
Unquoted outputStays one wordSplits into words
Array indexingStarts at 1 ()Starts at 0 ()
on an arrayEvery elementFirst element only
Recursive glob Built inNeeds
Unmatched globError: Pattern passed through as-is
Glob qualifiers ()YesNo, use
Config file
Prompt variable with escapes with escapes
Borrowing the other behaviour inside a functionn/a
Spelling correction, suffix aliasesYesNo

The three that break a pasted bash snippet:

files="a.txt b.txt"
ls $files      # zsh looks for one file named "a.txt b.txt"; use ${=files} to split

a=(x y z)
echo $a[1]     # zsh prints x; the same array in bash starts at ${a[0]}

ls *.nope      # zsh: "no matches found" and ls never runs; bash passes *.nope to ls

Scripts are unaffected: a shebang runs the script in bash whatever your login shell is. Write new scripts for the shell you name in the shebang.

Gotchas

  • when a URL contains or : zsh globs the argument. Quote it () or .
  • comments do not work at the interactive prompt by default: (Oh My Zsh turns this on for you).
  • on a command containing inside double quotes: history expansion fires before anything else. Use single quotes, or to switch it off.
  • when you try to exit: run to see them, to finish one, or to cut them loose. A second also works.
  • on a Mac binary you downloaded is Gatekeeper, not memory: . On a server it usually is memory, and will say so.
  • Pasted multi-line snippets run line by line as they land. Paste into (editor) if you want to review first.
  • A new PATH entry in only applies to new shells; fixes the current one.
  • zsh writes each terminal's history at exit by default; if you want tabs to see each other's commands live.
  • Long remote sessions die with the connection. Run zsh inside tmux on servers so your shell survives a dropped SSH connection.

zsh FAQ

Why zsh over bash?

Apple switched the macOS default in Catalina (2019) because the bash it shipped was stuck at version 3.2 from 2007; newer bash releases moved to the GPLv3 license, which Apple avoids. What you get over bash 3.2 day to day: completion that knows flags, hostnames, git branches and PIDs, glob qualifiers that replace most simple find calls, suffix and global aliases, and spelling correction on mistyped commands. What you give up is nothing, since your scripts keep running under their own shebang. On Linux bash is fine and stays the system shell; zsh is a choice about your interactive prompt, not about your servers.

Should I use .zshrc or .zprofile?

~/.zprofile runs once per login shell, ~/.zshrc runs for every interactive shell, and .zprofile runs first. PATH edits and eval "$(brew shellenv)" belong in .zprofile so they run once; aliases, functions, keybindings, completion and prompt config belong in .zshrc. The reason it matters is duplication: PATH lines in .zshrc run again in every nested shell, so PATH grows every time you type zsh or open a subshell. If you keep PATH in .zshrc anyway, add typeset -U path so duplicate entries collapse. On macOS every terminal tab is a login shell so both files run; on Linux terminals are usually non-login, so anything you put only in .zprofile is missing until you log in fresh.

Does zsh use .bashrc?

No. zsh never reads ~/.bashrc or ~/.bash_profile, which is why everything seems to vanish the day you switch. It does not read ~/.profile either unless you source it yourself. Move your aliases, functions, PATH exports and tool init lines (nvm, pyenv, conda, rbenv) into ~/.zshrc or ~/.zprofile by hand. Sourcing .bashrc from .zshrc mostly works for plain exports and aliases but breaks on shopt, complete, PROMPT_COMMAND and PS1 escapes, so copy the lines you want instead of sourcing the file.

Can zsh run bash scripts?

Yes. A script with a #!/bin/bash shebang runs in bash no matter which shell you launched it from, so your login shell never changes how a script behaves. The trap is scripts with no shebang, or bash snippets you paste straight into an interactive zsh prompt: there zsh's own rules apply, and three of them bite. Unquoted variables are not word-split, arrays are indexed from 1, and a glob that matches nothing is a hard error instead of being passed through literally. Run bash script.sh explicitly when you are unsure, and see the bash cheat sheet at /cheatsheets/bash for the syntax itself.

Why is zsh slow to start?

Almost always a plugin or a version manager, not zsh. Measure before you cut: add zmodload zsh/zprof as the first line of ~/.zshrc and run zprof in a new shell to get a ranked list of what ran. Common culprits are nvm (200ms or more on its own, fixable with lazy loading or by switching to fnm), conda init, pyenv init, and compinit rebuilding its cache on every start. Replace the plain compinit call with a once-a-day version: autoload -Uz compinit; compinit -C, with a full compinit run only when ~/.zcompdump is older than a day. If the prompt is the slow part, powerlevel10k's instant prompt paints the line before the rest of the config loads.

Related cheat sheets