← Back to DevBytes

Fish Scripting: Prompt Customization Complete Guide

Understanding Fish Prompt Customization

Fish shell (Friendly Interactive Shell) provides one of the most elegant and powerful prompt customization systems among modern shells. Unlike Bash or Zsh, which rely on cryptic escape sequences and complex substitution syntax, Fish uses a clean, function-based approach. Your entire prompt is defined by a function named fish_prompt that you write once and Fish calls automatically before displaying each prompt line.

What Is the Fish Prompt System?

The Fish prompt system revolves around three special functions that you define in your Fish configuration:

These functions are automatically sourced from your ~/.config/fish/functions/ directory or defined directly in ~/.config/fish/config.fish. Fish calls them every time it needs to render the prompt, which means your prompt can dynamically reflect the current state of your shell, Git repository, environment variables, or any other information you can access in a Fish function.

Why Prompt Customization Matters

A well-designed prompt transforms your terminal from a bland input line into an informative dashboard. Here's what a thoughtfully customized Fish prompt gives you:

Core Building Blocks

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

The fish_prompt Function

Every Fish prompt starts with this fundamental function. Fish looks for fish_prompt in your function path and executes it to generate the prompt string. The function must output (via echo) the text that will appear as your prompt. Here's the minimal valid prompt:

function fish_prompt
    echo "> "
end

This produces a simple > prompt. But the real power comes from combining Fish's built-in commands for color, path manipulation, and status checking.

Working with set_color

The set_color command is your primary tool for styling. It accepts color names, hex codes, and special modifiers:

# Named colors
set_color red
set_color cyan
set_color yellow

# Hex colors (must use 6-digit hex; 3-digit shorthand is not supported)
set_color ff6b6b
set_color 2ecc71

# Special options
set_color normal      # Reset to default terminal text color
set_color --bold      # Bold text
set_color --underline # Underlined text
set_color --italics   # Italic text (if terminal supports it)
set_color -b cyan     # Set background color
set_color --dim       # Dimmed/bright text

# Reset everything
set_color --bold red --background blue
# Always reset with:
set_color normal

Critically, set_color affects all subsequent output until you call set_color normal. Always reset colors at the end of your prompt function to avoid color bleeding into the user's typed commands.

Accessing Essential Prompt Information

Fish provides several variables and commands that are indispensable for prompt construction:

# Current working directory (shortened)
echo $PWD

# Custom path shortening with prompt_pwd
prompt_pwd           # Shortened path like ~/p/s/fish
prompt_pwd --full-length  # Full path
prompt_pwd --dir-length 1  # Show only first component of each directory

# Last command exit status
echo $status         # 0 for success, non-zero for failure

# Current user and host
echo $USER
echo $hostname       # or prompt_hostname

# Current time
date "+%H:%M:%S"

Building Your First Custom Prompt

A Simple Two-Segment Prompt

Let's build a prompt that shows the current directory and a prompt symbol that changes color based on the last command's success or failure:

function fish_prompt
    # Save the exit status immediately
    set -l last_status $status
    
    # Start with the current directory in blue
    set_color blue
    echo -n (prompt_pwd)
    
    # Add a space
    echo -n " "
    
    # Show prompt symbol: green arrow for success, red for failure
    if test $last_status -eq 0
        set_color green
        echo -n "❯"
    else
        set_color red
        echo -n "❯"
    end
    
    # Add a trailing space and reset color
    echo -n " "
    set_color normal
end

This produces a prompt like ~/Documents/project when the last command succeeded, or when it failed. Notice the critical pattern: always capture $status at the very top of your function. Any command you run inside fish_prompt (including echo, set_color, or test) will overwrite $status, so saving it first prevents losing the original exit code.

Adding User and Host Information

For SSH sessions or multi-user systems, showing user and host adds valuable context:

function fish_prompt
    set -l last_status $status
    
    # Show user@host in dim style
    set_color --dim cyan
    echo -n $USER@(prompt_hostname)
    echo -n " "
    
    # Directory in bright blue
    set_color blue --bold
    echo -n (prompt_pwd)
    
    # Reset bold but keep color context for symbol
    set_color normal
    
    echo -n " "
    
    # Prompt symbol
    if test $last_status -eq 0
        set_color green
        echo -n "❯"
    else
        set_color red
        echo -n "❯"
    end
    
    echo -n " "
    set_color normal
end

Integrating Git Information

Manual Git Status Detection

Fish can check if you're in a Git repository and display branch information using simple command substitution:

function fish_prompt
    set -l last_status $status
    
    # Check if we're in a git repository
    set -l git_branch (git rev-parse --abbrev-ref HEAD 2>/dev/null)
    
    if test -n "$git_branch"
        # We're in a git repo - show branch name in magenta
        set_color magenta
        echo -n " $git_branch"
        
        # Check for dirty state (unstaged changes)
        set -l git_dirty (git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
        if test "$git_dirty" -gt 0
            echo -n " ●"  # Dirty indicator
        else
            echo -n " ○"  # Clean indicator
        end
        
        echo -n " "
    end
    
    # Directory path
    set_color blue
    echo -n (prompt_pwd)
    echo -n " "
    
    # Prompt symbol
    if test $last_status -eq 0
        set_color green
        echo -n "❯"
    else
        set_color red
        echo -n "❯"
    end
    
    echo -n " "
    set_color normal
end

More Detailed Git Status

For a richer Git display, you can check specific states like staged changes, unstaged changes, and untracked files:

function fish_prompt
    set -l last_status $status
    
    # Git branch detection
    set -l git_branch (git rev-parse --abbrev-ref HEAD 2>/dev/null)
    set -l git_dir (git rev-parse --git-dir 2>/dev/null)
    
    if test -n "$git_dir"
        set_color magenta --bold
        echo -n " $git_branch"
        
        # Detailed status indicators
        set -l git_staged (git diff --staged --name-only 2>/dev/null | wc -l | string trim)
        set -l git_unstaged (git diff --name-only 2>/dev/null | wc -l | string trim)
        set -l git_untracked (git ls-files --others --exclude-standard 2>/dev/null | wc -l | string trim)
        
        if test "$git_staged" -gt 0
            set_color green
            echo -n " +$git_staged"
        end
        
        if test "$git_unstaged" -gt 0
            set_color yellow
            echo -n " ~$git_unstaged"
        end
        
        if test "$git_untracked" -gt 0
            set_color red
            echo -n " ?$git_untracked"
        end
        
        set_color normal
        echo -n " "
    end
    
    set_color blue
    echo -n (prompt_pwd)
    echo -n " "
    
    if test $last_status -eq 0
        set_color green
        echo -n "❯"
    else
        set_color red
        echo -n "❯"
    end
    
    echo -n " "
    set_color normal
end

The Right Prompt: fish_right_prompt

The right prompt appears on the same line as your main prompt but aligned to the right edge of the terminal. It's perfect for secondary information like the current time, Python virtual environment, or Node.js version. Fish automatically handles the alignment — your function simply outputs the content:

function fish_right_prompt
    # Show current time in dim style
    set_color --dim brblack
    echo -n (date "+%H:%M:%S")
    
    # Show Python virtual environment if active
    if set -q VIRTUAL_ENV
        set_color cyan
        echo -n " 🐍 "(basename "$VIRTUAL_ENV")
    end
    
    set_color normal
end

The right prompt gracefully disappears if your command line extends far enough to collide with it. Fish handles this collision automatically by hiding the right prompt when the cursor approaches it.

Advanced Right Prompt with Duration Display

One popular pattern is showing how long the last command took to execute. Fish provides $CMD_DURATION — the runtime of the previous command in milliseconds:

function fish_right_prompt
    set_color --dim brblack
    
    # Show last command duration if it took more than 3 seconds
    if test -n "$CMD_DURATION" -a "$CMD_DURATION" -gt 3000
        set -l duration_seconds (math -s0 "$CMD_DURATION / 1000")
        echo -n "⏱ {$duration_seconds}s "
    end
    
    # Show current time
    echo -n (date "+%H:%M")
    
    # Show error count if last command failed
    if test $status -ne 0
        set_color red
        echo -n " ✗"
    end
    
    set_color normal
end

Vi Mode Prompt Customization

When Fish's vi key bindings are active (enabled with fish_vi_key_bindings), the fish_mode_prompt function controls the mode indicator. This function is called only in vi mode to show whether you're in insert, normal, visual, or replace mode:

function fish_mode_prompt
    # Only show mode indicator in non-insert mode to minimize noise
    switch $fish_bind_mode
        case default
            # Insert mode - no indicator (clean prompt)
            echo -n ""
        case insert
            echo -n ""
        case replace_one
            set_color green --bold
            echo -n " [REPLACE] "
            set_color normal
        case visual
            set_color magenta --bold
            echo -n " [VISUAL] "
            set_color normal
        case replace
            set_color yellow --bold
            echo -n " [REPLACE] "
            set_color normal
    end
end

The $fish_bind_mode variable contains the current vi mode. The mode prompt is typically displayed at the beginning of the prompt line, before the output of fish_prompt, making it a natural prefix.

Complete Production-Grade Prompt Example

Here's a full-featured prompt combining all the techniques discussed, suitable for daily development use. Save this as ~/.config/fish/functions/fish_prompt.fish:

function fish_prompt
    # ─── Capture exit status immediately ───
    set -l last_status $status
    
    # ─── Vi mode indicator (only in non-insert modes) ───
    if test "$fish_key_bindings" = fish_vi_key_bindings
        switch $fish_bind_mode
            case replace_one replace
                set_color green --bold
                echo -n "[R] "
            case visual
                set_color magenta --bold
                echo -n "[V] "
        end
        set_color normal
    end
    
    # ─── Git section ───
    set -l git_branch (git rev-parse --abbrev-ref HEAD 2>/dev/null)
    set -l git_dir (git rev-parse --git-dir 2>/dev/null)
    
    if test -n "$git_dir"
        set_color magenta --bold
        echo -n " $git_branch"
        
        # Check for dirty state efficiently
        set -l git_dirty (git status --porcelain --ignore-submodules 2>/dev/null | string collect)
        if test -n "$git_dirty"
            set_color yellow
            echo -n " ●"
        else
            set_color green
            echo -n " ○"
        end
        
        # Check for stash
        set -l git_stash (git stash list 2>/dev/null | count)
        if test "$git_stash" -gt 0
            set_color cyan
            echo -n " ⚑"
        end
        
        set_color normal
        echo -n " "
    end
    
    # ─── Directory section ───
    set_color blue --bold
    echo -n (prompt_pwd)
    
    # ─── Prompt symbol ───
    echo -n " "
    
    if test $last_status -eq 0
        set_color green --bold
        echo -n "❯"
    else
        set_color red --bold
        echo -n "❯"
        # Optionally show the exit code for non-zero status
        set_color yellow
        echo -n " [$last_status]"
    end
    
    echo -n " "
    set_color normal
end

function fish_right_prompt
    set_color --dim brblack
    
    # ─── Command duration ───
    if test -n "$CMD_DURATION" -a "$CMD_DURATION" -gt 2000
        set -l secs (math -s0 "$CMD_DURATION / 1000")
        echo -n "⏱ {$secs}s "
    end
    
    # ─── Python virtual environment ───
    if set -q VIRTUAL_ENV
        set_color cyan --dim
        echo -n "🐍 "(basename "$VIRTUAL_ENV")" "
    end
    
    # ─── Node version if in a node project ───
    if test -f package.json
        set -l node_ver (node -v 2>/dev/null)
        if test -n "$node_ver"
            set_color green --dim
            echo -n "⬢ $node_ver "
        end
    end
    
    # ─── Time ───
    echo -n (date "+%H:%M")
    set_color normal
end

Performance Considerations

Why Prompt Speed Matters

Your prompt function runs every time you press Enter. A slow prompt creates a perceptible lag between commands that compounds throughout your workday. Fish prompts should target sub-50ms execution time. The most common performance pitfalls are:

Optimizing Git Checks

Instead of running full git status, use targeted Git commands that return faster:

# Fast: Check only if branch exists
set -l git_branch (git branch --show-current 2>/dev/null)

# Faster than git status for dirty check on large repos:
set -l git_dirty (git diff --quiet 2>/dev/null; echo $status)

# Avoid: This scans the entire working tree
# git status --porcelain   # Slow on large repos!

Caching with Asynchronous Prompts

For complex Git displays, consider moving Git checks to a background job that updates a cached variable. Fish supports this pattern through its event system:

# Define a variable to cache git info
set -g __fish_git_info ""

function __fish_update_git_info --on-event fish_prompt
    # Run git checks in a subshell spawned asynchronously
    fish -c "
        set -l branch (git branch --show-current 2>/dev/null)
        if test -n \"\$branch\"
            set -l dirty (git diff --quiet 2>/dev/null; or echo 'dirty')
            echo \"\$branch|\$dirty\" > /tmp/fish_git_cache.\$fish_pid
        end
    " &
end

function fish_prompt
    # Read from cache
    set -l git_cache_file /tmp/fish_git_cache.(string replace -a '%' '' $fish_pid)
    if test -f "$git_cache_file"
        set -l git_data (cat "$git_cache_file")
        # Parse and display...
    end
    # ... rest of prompt
end

Best Practices Summary

Troubleshooting Common Issues

Prompt Colors Bleeding Into Commands

If your typed commands appear in the wrong color, you've forgotten set_color normal at the end of your prompt function. Add it as the very last command before the function returns.

Prompt Not Updating

If changes to fish_prompt don't take effect, ensure:

Git Info Shows for Non-Git Directories

Always check both git_branch and git_dir before displaying Git information. A bare git rev-parse --abbrev-ref HEAD can return "HEAD" even outside a repository in some edge cases. Testing git_dir (which returns the .git directory path) is more reliable.

Right Prompt Disappears

This is expected behavior — Fish hides the right prompt when your command line approaches it to prevent overlap. If you want the right prompt always visible, keep it short (under 30 characters) or use a wider terminal.

Going Further: Conditional Prompts

You can create different prompts for different contexts by checking environment variables or directory characteristics:

function fish_prompt
    set -l last_status $status
    
    # Production server warning
    if test -f /etc/production-server
        set_color red --bold
        echo -n "⚠ PRODUCTION "
        set_color normal
    end
    
    # Different prompts for specific projects
    switch (basename $PWD)
        case my-web-app
            set_color cyan
            echo -n "🌐 web-app "
        case api-server
            set_color magenta
            echo -n "🔌 api "
    end
    
    # ... rest of prompt
end

Conclusion

Fish prompt customization represents one of the shell's most elegant design decisions. By reducing prompt configuration to a simple function with clear, composable building blocks like set_color, prompt_pwd, and $status, Fish makes prompt design accessible while remaining powerful enough for complex setups. The function-based approach means your prompt is just Fish code — you can use conditionals, loops, external commands, and any logic you need. Start with a simple working prompt, add Git information, then layer on right-prompt details like time and command duration. Keep performance in mind by using fast Git checks and avoiding network calls. With the techniques covered here, you can craft a prompt that serves as a genuine productivity tool rather than just decoration. Your prompt becomes a heads-up display for your terminal workflow, giving you exactly the information you need at exactly the moment you need it.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles