← Back to DevBytes

Zsh Scripting: Prompt Customization Complete Guide

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

Understanding Zsh Prompt Variables

Zsh exposes several prompt-related variables. The most important ones are:

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

Directory Information

Status and Time

Formatting

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:

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles