Introduction to Zsh Prompt Customization
The Z shell (Zsh) is renowned for its powerful, flexible prompt system. Unlike Bash, which offers relatively limited prompt customization through the PS1 variable, Zsh provides a multi-prompt architecture, escape sequences, conditional expressions, and hooks that let you build a prompt as simple or as elaborate as you want. Whether you want a minimal two-line prompt or a feature-rich display with git status, execution time, and color-coded indicators, Zsh can deliver.
This guide walks through everything from the basics of prompt variables to advanced techniques like async prompt rendering and theme design. By the end, you'll be able to craft a prompt that fits your workflow perfectly.
Why Prompt Customization Matters
Your shell prompt is the single piece of UI you interact with most as a developer. A well-designed prompt gives you instant context: where you are, what branch you're on, whether commands succeeded, and how long they took. A poorly designed prompt, on the other hand, wastes space, slows down your terminal, or hides information you actually need.
Benefits of a Custom Prompt
- Situational awareness: Show git branch, virtual environment, or Kubernetes context at a glance.
- Error visibility: Color-code the prompt based on the exit code of the last command.
- Productivity: Reduce the number of times you run
pwd,git status, orgit branch. - Aesthetics: A pleasant terminal makes long coding sessions more enjoyable.
- Performance awareness: Display command execution time to catch slow operations early.
Understanding Zsh Prompt Variables
Zsh exposes several prompt-related variables. The most important ones are:
PS1/PROMPT: The primary prompt, shown after each command.PS2/PROMPT2: The continuation prompt, shown when a command spans multiple lines.RPS1/RPROMPT: A right-aligned prompt that appears on the right side of the terminal.PS3: Used by theselectbuiltin.PS4: Used for tracing execution withset -x.
PROMPT and PS1 are aliases for the same variable; PROMPT is the more idiomatic name in Zsh. The same applies to RPROMPT and RPS1.
A First Example
Open your ~/.zshrc and add the following line, then reload with source ~/.zshrc:
PROMPT='%n@%m:%~$ '
This produces a prompt like alice@laptop:~/projects$ . The % sequences are Zsh prompt escapes, which we'll cover in detail next.
Prompt Escape Sequences
Zsh uses %-prefixed escape sequences inside prompt strings. These are far richer than Bash's \u, \h style escapes. Here are the most commonly used ones:
Identity and Host
%n— current username.%m— hostname up to the first dot.%M— full hostname.%l— the tty the shell is running on.
Directory Information
%~— current directory with~substitution (e.g.,~/projects).%d/%/— absolute path of current directory.%c/%C— trailing component of the path (basename).%2~— last two components of the path with~substitution.
Status and Time
%?— exit code of the last command.%#—#if running as root,%otherwise.%T— current time in 24-hour format.%t— current time in 12-hour format with AM/PM.%D— current date inyy-mm-ddformat.%j— number of background jobs.
Formatting
%%— a literal percent sign.%)— a literal closing parenthesis (useful inside conditional groups).%B/%b— start / stop bold.%U/%u— start / stop underline.%F{color}/%f— start / stop foreground color.%K{color}/%k— start / stop background color.
Putting Sequences Together
PROMPT='%F{cyan}%n%f@%F{green}%m%f %F{blue}%~%f %# '
This colors the username cyan, the hostname green, and the directory blue. The %f resets the foreground color back to default after each segment.
Colors in Zsh Prompts
Zsh supports both named colors and 256-color codes. Named colors include black, red, green, yellow, blue, magenta, cyan, white, and default. For 256-color terminals, you can use numeric codes from 0 to 255.
Named Colors
PROMPT='%F{red}ERROR%f %F{green}OK%f '
256 Colors
PROMPT='%F{208}warm-orange%f %F{33}deep-blue%f '
To preview all 256 colors in your terminal, run:
for i in {0..255}; do print -P "%F{$i}Color $i%f"; done
True Color (24-bit)
If your terminal supports true color, you can use hex codes via the %F escape with a # prefix:
PROMPT='%F{#ff8800}orange%f %F{#0088ff}blue%f '
Note that true color support depends on both your terminal emulator and the TERM value. Most modern terminals (iTerm2, Alacritty, Kitty, Windows Terminal) support it.
Building a Two-Line Prompt
Two-line prompts are popular because they give you room for information on the first line while keeping the input area clean. Use a literal newline in the prompt string:
PROMPT='%F{cyan}%~%f
%F{green}%#%f '
Or use the $'\n' syntax for clarity:
PROMPT=$'%F{cyan}%~%f\n%F{green}%#%f '
Adding Git Information
Zsh ships with a version-control info system via the vcs_info function. To use it, you need to load it and hook it into precmd:
autoload -Uz vcs_info
precmd() { vcs_info }
zstyle ':vcs_info:git:*' formats ' %F{yellow}(%b)%f'
setopt PROMPT_SUBST
PROMPT='%F{cyan}%~%f${vcs_info_msg_0_}
%F{green}%#%f '
Several things are happening here:
autoload -Uz vcs_infoloads the function without aliases.precmd()runsvcs_infobefore each prompt is drawn.zstyleconfigures the format string for git repositories.%bis the branch name.setopt PROMPT_SUBSTenables variable and command substitution inside the prompt string, which is required for${vcs_info_msg_0_}to be expanded.
Conditional Prompt Elements
Zsh supports conditional expressions inside prompts using the %(...true...false...) syntax. This is invaluable for showing information only when relevant.
Showing Root vs. User
PROMPT='%(!.%F{red}%n%f.%F{green}%n%f)@%m %# '
The %(!.A.B) form means "if the shell is running as root, show A, otherwise show B." Here the username is red for root and green for regular users.
Showing the Last Exit Code Only on Failure
PROMPT='%(?.%F{green}✓%f.%F{red}[%?]✗%f) %~ %# '
The %(?.A.B) form checks the last exit code. If it's zero (success), it shows a green checkmark. If non-zero, it shows a red X with the exit code in brackets.
Nested Conditionals
You can nest conditionals and combine them with other escapes. For example, show the number of background jobs only when there are any:
PROMPT='%1(j.%F{magenta}[%j jobs]%f .)%~ %# '
The %N(j.A.B) form checks if the number of background jobs is at least N. Here, if there's at least one job, it shows [N jobs] in magenta; otherwise it shows nothing.
Right-Side Prompts
The RPROMPT variable places content on the right edge of the terminal. It's perfect for secondary information that you want visible but not in your way:
PROMPT='%F{cyan}%~%f %# '
RPROMPT='%F{yellow}%T%f'
This shows the directory and prompt symbol on the left and the current time on the right. The right prompt automatically disappears when the input line reaches it, so it never interferes with typing.
Combining RPROMPT with Git Status
autoload -Uz vcs_info
precmd() { vcs_info }
zstyle ':vcs_info:git:*' formats '%b'
zstyle ':vcs_info:git:*' actionformats '%b|%a'
setopt PROMPT_SUBST
PROMPT='%F{cyan}%~%f %# '
RPROMPT='%F{yellow}${vcs_info_msg_0_}%f'
Dynamic Prompts with Functions
For complex logic, you can call functions from within the prompt. With PROMPT_SUBST enabled, any $(...) or ${...} is evaluated each time the prompt is drawn.
Example: Git Branch Function
git_branch() {
local ref
ref=$(git symbolic-ref --short HEAD 2>/dev/null) || return
echo "%F{magenta} ($ref)%f"
}
setopt PROMPT_SUBST
PROMPT='%F{cyan}%~%f$(git_branch)
%F{green}%#%f '
Example: Command Execution Time
You can measure how long each command takes and display it in the next prompt. This requires a preexec hook to record the start time and a precmd hook to compute the elapsed time:
typeset -g _cmd_start_time
preexec() {
_cmd_start_time=$EPOCHREALTIME
}
precmd() {
if [[ -n $_cmd_start_time ]]; then
local elapsed=$(( EPOCHREALTIME - _cmd_start_time ))
if (( elapsed >= 1 )); then
_cmd_duration="%F{red}${elapsed}s%f"
else
_cmd_duration=""
fi
unset _cmd_start_time
else
_cmd_duration=""
fi
}
zmodload zsh/datetime
setopt PROMPT_SUBST
PROMPT='${_cmd_duration} %F{cyan}%~%f %# '
The zsh/datetime module provides $EPOCHREALTIME, which gives sub-second precision. The prompt only shows the duration if the command took at least one second.
Using Prompt Themes
Zsh ships with a prompt theme system that bundles several ready-made prompts. To use it:
autoload -Uz promptinit
promptinit
prompt adam1
To list available themes:
prompt -l
To preview them all:
prompt -p
While the built-in themes are convenient, most developers eventually move to either a custom prompt or a third-party framework like Starship, Powerlevel10k, or Pure, which offer richer features out of the box.
Powerline and Special Glyphs
Many modern prompts use special glyphs from Nerd Fonts — arrows, branch icons, and separators. For example:
PROMPT='%F{blue}%~%f %F{magenta} $(git_branch)%f
%F{green}❯%f '
To use these reliably, install a Nerd Font (such as MesloLGS NF or JetBrainsMono Nerd Font) and configure your terminal emulator to use it. Without the correct font, glyphs will appear as boxes or question marks.
A Powerline-Style Segment Prompt
setopt PROMPT_SUBST
git_branch() {
local ref
ref=$(git symbolic-ref --short HEAD 2>/dev/null) || return
echo " $ref"
}
PROMPT=$'%F{black}%K{blue} %~ %k%f%F{blue}%K{magenta}$(git_branch)%k%f%F{magenta}%k%f
%F{green}❯%f '
This uses background colors (%K) and the Powerline arrow character to create connected segments. Adjust the arrow glyph based on your font.
Asynchronous Prompt Rendering
If your prompt calls slow commands (like git status on a large repo, or network queries), it can introduce noticeable lag. Asynchronous prompt rendering solves this by computing expensive parts in a background job and updating the prompt when results are ready.
The zsh-async library and frameworks like Powerlevel10k handle this for you. Here's a simplified manual approach using a background job:
setopt PROMPT_SUBST
typeset -g _git_info=""
update_git_info() {
local ref
ref=$(git symbolic-ref --short HEAD 2>/dev/null)
if [[ -n $ref ]]; then
_git_info="%F{magenta} ($ref)%f"
else
_git_info=""
fi
# Force a prompt redraw
zle reset-prompt
}
precmd() {
# Run git info update in the background
(update_git_info &) 2>/dev/null
}
PROMPT='%F{cyan}%~%f${_git_info}
%F{green}%#%f '
In practice, you'd want more robust handling with zsh-async or zle widgets, but this illustrates the concept: the prompt draws immediately with stale or empty git info, then refreshes once the background job completes.
Best Practices
Keep It Fast
Your prompt runs before every single command. Even a 50ms delay becomes irritating over time. Avoid calling slow external commands synchronously. Cache results where possible, and use asynchronous rendering for expensive operations.
Use PROMPT_SUBST Judiciously
setopt PROMPT_SUBST is powerful but can be a security concern if you ever cd into untrusted directories with crafted filenames. Be cautious about embedding raw directory names that could contain prompt escape sequences.
Test in a Subshell
Before committing prompt changes to ~/.zshrc, test them in a subshell so a syntax error doesn't lock you out of your shell:
zsh -c 'source /tmp/test_prompt.zsh; exec zsh'
Keep Prompts Readable
Long prompt strings with many escapes become hard to maintain. Break them into variables or functions:
setopt PROMPT_SUBST
prompt_user='%F{green}%n%f@%F{cyan}%m%f'
prompt_dir='%F{blue}%~%f'
prompt_symbol='%(?.%F{green}❯%f.%F{red}❯%f)'
PROMPT='${prompt_user} ${prompt_dir}
${prompt_symbol} '
Handle Missing Tools Gracefully
If your prompt calls git, node, or kubectl, make sure it degrades gracefully when those tools aren't installed or when you're outside a relevant context. Always redirect stderr to /dev/null for commands that might fail.
Respect Terminal Width
On narrow terminals, long prompts wrap awkwardly. Consider truncating paths with %50~ (show last 50 characters of the path) or conditionally hiding segments based on $COLUMNS.
Version Control Your Dotfiles
Once you've invested time in your prompt, store your ~/.zshrc in a dotfiles repository. This makes it easy to replicate your environment across machines and roll back changes if something breaks.
Putting It All Together: A Complete Prompt
Here's a complete, production-ready prompt that combines everything covered: colors, git info, exit code awareness, execution time, and a clean two-line layout:
# ~/.zshrc prompt section
zmodload zsh/datetime
autoload -Uz vcs_info
setopt PROMPT_SUBST
# --- Git info via vcs_info ---
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:git:*' formats '%b'
zstyle ':vcs_info:git:*' actionformats '%b|%a'
# --- Execution time tracking ---
typeset -g _cmd_start_time
typeset -g _cmd_duration=""
preexec() {
_cmd_start_time=$EPOCHREALTIME
}
precmd() {
vcs_info
if [[ -n $_cmd_start_time ]]; then
local elapsed=$(( EPOCHREALTIME - _cmd_start_time ))
if (( elapsed >= 2 )); then
_cmd_duration="%F{red}took ${elapsed}s%f "
elif (( elapsed >= 0.5 )); then
_cmd_duration="%F{yellow}took ${elapsed}s%f "
else
_cmd_duration=""
fi
unset _cmd_start_time
fi
}
# --- Prompt components ---
local user_host='%F{green}%n%f@%F{cyan}%m%f'
local current_dir='%F{blue}%~%f'
local git_info='%F{magenta}${vcs_info_msg_0_:+ ($vcs_info_msg_0_)}%f'
local status_symbol='%(?.%F{green}❯%f.%F{red}❯ [%?]%f)'
PROMPT='${user_host} ${current_dir}${git_info}
${_cmd_duration}${status_symbol} '
RPROMPT='%F{yellow}%T%f'
This prompt shows the user and host on the first line, followed by the directory and git branch. The second line shows the execution time (if notable) and a status symbol that turns red with the exit code on failure. The right prompt displays the current time.
Conclusion
Zsh's prompt system is one of its most powerful features, offering a depth of customization that goes far beyond what most shells provide. By mastering escape sequences, conditional expressions, color codes, and hooks like precmd and preexec, you can build a prompt that delivers exactly the context you need without sacrificing performance. Start simple, iterate as your needs grow, and always keep an eye on speed — a fast, informative prompt is one of the highest-leverage productivity tweaks available to any developer working in the terminal.