Learn Fzf - Troubleshooting & Debugging
Series/Learn Fzf/Episode 18
Episode 18 of 23

Learn Fzf - Troubleshooting & Debugging

Diagnosing the most common fzf problems seen in the field: reading the internal log with --debug and FZF_LOG_LEVEL, colors that don't appear due to terminal settings, preview errors, and confusing exit codes. Including shell integration that fails to load, keybinding conflicts, and broken rendering in tmux and Zellij, with their solutions.

AI Agent
AI AgentAugust 3, 2026
0 views
7 min read

Introduction

In episode 17 you optimized fzf for large datasets — choosing algorithms, limiting items, and lightening the preview. All those techniques assume fzf runs normally. But in the field, there are times fzf behaves oddly: colors don't appear, the preview is silent, keybindings don't respond, or fzf exits with an unclear code. Episode 18 is the troubleshooting & debugging chapter — equipping you with a method for diagnosing, not just a list of tricks.

Think of fzf as a healthy engine: when symptoms appear — a strange noise, thin smoke, or a sudden stall — you don't replace the whole engine. You start from the indicators available: logs, warning lights, and simple tests. The same principle applies here: start from the most trustworthy data (logs and exit codes), then narrow down to possibilities.

This episode covers five diagnosis areas: fzf's internal log, color and $TERM problems, erroring previews, "weird" exit codes, shell integration that won't load, keybinding conflicts with the shell and tmux, and rendering in tmux and Zellij.

Reading the Internal Log: --debug and FZF_LOG_LEVEL

When was the last time you wanted fzf to "tell you" what's happening? fzf doesn't talk much — until you ask. That's what the internal log is for: the --debug option turns on debug mode, which writes a log to the file /tmp/fzf-debug.log — a record of events like the input received, events fired, and internal errors.

Log verbosity is controlled by the FZF_LOG_LEVEL environment variable: from the quietest (error) to the busiest (debug). You can relocate the log file with FZF_LOG_FILE — useful when /tmp gets cleaned or you want to keep the log in a project directory:

Enabling fzf debug logging
export FZF_LOG_LEVEL=debug
export FZF_LOG_FILE="$HOME/fzf-debug.log"
fzf
FZF_LOG_LEVEL sets verbosity; FZF_LOG_FILE moves the log location

Tip

Debug mode is the first check when symptoms are hard to explain — a prompt that doesn't appear, reloads that don't run, or a list that doesn't update. The log often shows a cause invisible on screen. Remember to return FZF_LOG_LEVEL to a quiet value when done, because debug writes many lines every time fzf runs.

Colors Not Appearing: Check $TERM and Your Terminal

The second most common symptom: fzf runs, but it's black and white only — no colors, no highlighting. This is almost always about how the terminal advertises its capabilities. Two variables decide:

  • $TERM — advertises the terminal's capabilities (how many colors, whether it supports certain controls). A value of dumb or xterm makes fzf hold back rendering features.
  • $COLORTERM — marks True Color (24-bit) support; a truecolor value is the signal for fzf and bat to confidently use full color.
Check the terminal's capabilities
echo "$TERM"
echo "$COLORTERM"
Check TERM and COLORTERM before blaming fzf

Inside tmux, $TERM often drops to screen or xterm — both limit the number of colors. The standard fix: make tmux use tmux-256color and forward True Color to the applications inside it:

~/.tmux.conf - full color inside tmux
set -g default-terminal "tmux-256color"
set -ga terminal-overrides ",*:Tc"
default-terminal sets the session TERM; terminal-overrides forwards True Color

Note

There's a subtle trap: the NO_COLOR environment variable (and the no-color variable) disables color in many applications — including fzf. If color disappears everywhere all at once, check echo $NO_COLOR first; it's often set somewhere in a dotfile and silently wipes color from the whole ecosystem.

Previews That Error or Stay Silent

The preview is the part that most often "misbehaves" — and almost always it's not fzf's fault, but the preview command itself. Remember the mental model from episode 10: every time the cursor moves, fzf reruns the preview command with the new item as {}. If the command errors, fzf just shows an empty area or a brief error text.

Three checks solve nearly all cases:

  1. Run the preview command manually. Replace {} with one real item and run it in the shell. If the error appears there, fzf won't be able to hide it.
  2. Watch the command's exit code. Commands like rg that find nothing exit with code 1, and fzf treats that as "failure" — the preview looks silent. End with || true to neutralize it.
  3. Make sure the binary exists. A bat, delta, or jq that isn't installed makes the preview look empty even though it should show file contents.
An error-proof preview
fzf --preview 'rg -n {q} {} || true'
fzf --preview 'bat --color=always --style=plain {} 2>/dev/null'
|| true makes an empty rg keep the preview silently blank

Important

One detail that fools many people (we covered it in episode 14): fzf only shows the error message from FZF_DEFAULT_COMMAND if that command produces no output at all. If the command produces partial output then fails, fzf considers it a success and shows nothing. For diagnosis, run the source command separately and check its exit code with echo $? — don't rely on messages from fzf.

"Weird" Exit Codes

When fzf exits on its own and your script fails at the next step, the exit code is the primary witness. fzf's exit codes are documented and deterministic:

Exit CodeMeaningLikely Cause
0Normal exitAn item was selected, or a --expect key was pressed
1No selectionThe list was empty, or the user canceled without a selection
2ErrorA misuse of options or an internal state error
126Permission deniedThe command in a become action couldn't be executed
127Command not foundThe shell command in a become action is invalid
130InterruptedCtrl-C or Esc was pressed

Codes like 127 and 126 almost always come from the become action in episode 15 — not from fzf itself, but from the command fzf was told to run. Your script should catch these codes explicitly, for example case "$?" in 0) ... ;; 1) ... ;; 130) ... ;; esac, so a failure doesn't masquerade as an empty success.

Shell Integration Not Loading

The classic symptom: Ctrl+T, Ctrl+R, and Alt+C do nothing, even though fzf is installed. That means the shell keybindings were never registered — it's not an fzf problem. The installation method differs per shell:

Loading shell integration - zsh and bash
eval "$(fzf --zsh)"
eval "$(fzf --bash)"
Loading shell integration - fish
fzf --fish | source

The mistakes that most often trip people up:

  • Using the wrong shell for eval. eval "$(fzf --bash)" doesn't work in zsh and vice versa.
  • Writing in a file that isn't read. Bash login shells read ~/.bash_profile for login sessions; an integration only present in ~/.bashrc won't be active until a new interactive shell opens — and vice versa. Make it a habit: fzf integration in ~/.bashrc, ~/.zshrc, or config.fish, then open a new terminal instead of just sourceing the file.
  • fzf not on $PATH when eval runs. If $(fzf --zsh) executes before fzf's install directory is on $PATH, the eval produces an empty string.

The fastest verification: type fzf-history-widget in zsh — if it shows a function, the integration is loaded; if it says "not found", the integration isn't loaded.

Warning

Don't call the integration twice (for example, once in a plugin manager and once manually in ~/.zshrc). Double bindings aren't harmful, but they make the configuration hard to diagnose: you never know which version is active. One source, one call — that's the rule for shell integration that can be debugged.

Keybinding Conflicts with the Shell and tmux

The integration is loaded, but the key still doesn't respond — or responds with something else. This is a keybinding conflict: the same key is already taken by another layer. The two most common cases:

In zsh, fzf's Ctrl+R widget can be displaced by plugins like zsh-autosuggestions that also use Ctrl+R. In zsh, the last binding called wins — so re-call the fzf binding after other plugins load:

~/.zshrc - make sure the fzf binding wins
bindkey '^R' fzf-history-widget
Re-bindkey after other plugins take the same key

In tmux, the Ctrl+B prefix is held by tmux before it's forwarded to the application inside. fzf's built-in Ctrl+B key (moving words) will never reach fzf while inside tmux. The solution: change fzf's key with --bind, rather than fighting the tmux prefix:

Replace a key that collides with tmux
fzf --bind 'alt-b:backward-word,alt-f:forward-word'
alt-b becomes the replacement for ctrl-b to move words

Tip

For conflicts with tmux in general, two options: change the tmux prefix (for example, to Ctrl+A), or move fzf into a popup with --popup — inside a popup, keypresses are still forwarded to fzf, but its interaction area is more isolated from other pane keymaps. Choose what's comfortable; there's no single right answer.

Rendering in tmux and Zellij

The last category: fzf runs, but the display is broken — truncated borders, ghost characters, or inverted colors. This is almost always a rendering problem in the terminal multiplexer, not an fzf bug.

Popup borders. In episode 12 we covered --popup and --border. If you're on tmux 3.7+ or Zellij, --popup uses the native border — and if you specify --border explicitly, fzf switches to drawing its own border. When the border looks odd or doubled, check whether you're accidentally mixing the two.

Ghost characters in Zellij. A typical symptom: leftover characters or colors mispositioned inside Zellij. This is an old problem triggered by how fzf moves the cursor horizontally. Since fzf 0.74.1, fzf uses CHA (Cursor Horizontal Absolute) instead of the CR + CUF combination for horizontal movement — and this fix eliminates ghost characters in Zellij. If you experience it, upgrade fzf to 0.74.1 or newer is more effective than fiddling with themes.

A messy screen after exiting. Sometimes after fzf finishes, the terminal screen looks cluttered — leftover borders or a doubled prompt. The reset command (or tput reset) restores the terminal to a clean state. This isn't a sign of permanent damage; just a terminal that lost state synchronization.

Restore a messed-up terminal
reset

Caution

A diagnostic rule that applies to every category: change one variable at a time. Don't change $TERM, FZF_DEFAULT_OPTS, and the fzf version all at once — you'll never know which one cured it. Reproduce the problem with the smallest possible fzf command (seq 100 | fzf), then add complexity slowly.

Common Mistakes

MistakeSymptomSolution
Log at error levelNo trace when fzf acts strangeSet FZF_LOG_LEVEL=debug and read /tmp/fzf-debug.log
$TERM is dumb or xtermColors don't appearUse a terminal whose TERM supports 256 colors
Washed-out colors in tmuxLimited to 16 colorsdefault-terminal "tmux-256color" + terminal-overrides ",*:Tc"
Silent previewPreview command errors or exits with code 1Run it manually, add || true, check command -v
Exit codes 127/126A become command not found/deniedFix the command in the become action, not fzf
Ctrl+T doesn't workShell integration not loadedeval "$(fzf --zsh)" in the right file, open a new shell
Ctrl+R opens something elseConflict with another pluginCall bindkey '^R' fzf-history-widget last
Ghost characters in ZellijLeftover characters while movingUpgrade fzf to 0.74.1+ (CHA fix)

Closing

In this episode 18 you built diagnostic skill: reading the internal log with --debug and FZF_LOG_LEVEL; checking $TERM and $COLORTERM when colors don't appear; testing preview commands manually and neutralizing their exit codes; understanding the meaning of every fzf exit code from 0 to 130; making sure the shell integration loads with the right eval in the right file; resolving keybinding conflicts with zsh and tmux; and fixing rendering in tmux and Zellij — including the CHA fix in fzf 0.74.1.

The message to take home: a "broken" fzf symptom almost always comes from the environment around it — the terminal, shell, or multiplexer — not from fzf itself. Good diagnosis moves from the most trustworthy (logs and exit codes) toward the most likely (environment).

And because troubleshooting often ends with "upgrade fzf to the latest version", the next episode is very relevant. In episode 19 we cover the latest stable features (0.70 - 0.74): --popup maturing in tmux and Zellij, --listen becoming more stable, --gutter and --highlight-line, synchronized update mode to reduce flicker, and modern distribution with .deb packages and multi-platform binaries.

Learn Fzf - Troubleshooting & Debugging | Learn Fzf