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.

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.
--debug and FZF_LOG_LEVELWhen 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:
export FZF_LOG_LEVEL=debug
export FZF_LOG_FILE="$HOME/fzf-debug.log"
fzfTip
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.
$TERM and Your TerminalThe 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.echo "$TERM"
echo "$COLORTERM"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:
set -g default-terminal "tmux-256color"
set -ga terminal-overrides ",*:Tc"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.
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:
{} with one real item and run it in the shell. If the error appears there, fzf won't be able to hide it.rg that find nothing exit with code 1, and fzf treats that as "failure" — the preview looks silent. End with || true to neutralize it.bat, delta, or jq that isn't installed makes the preview look empty even though it should show file contents.fzf --preview 'rg -n {q} {} || true'
fzf --preview 'bat --color=always --style=plain {} 2>/dev/null'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.
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 Code | Meaning | Likely Cause |
|---|---|---|
0 | Normal exit | An item was selected, or a --expect key was pressed |
1 | No selection | The list was empty, or the user canceled without a selection |
2 | Error | A misuse of options or an internal state error |
126 | Permission denied | The command in a become action couldn't be executed |
127 | Command not found | The shell command in a become action is invalid |
130 | Interrupted | Ctrl-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.
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:
eval "$(fzf --zsh)"
eval "$(fzf --bash)"fzf --fish | sourceThe mistakes that most often trip people up:
eval "$(fzf --bash)" doesn't work in zsh and vice versa.~/.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.$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.
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:
bindkey '^R' fzf-history-widgetIn 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:
fzf --bind 'alt-b:backward-word,alt-f:forward-word'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.
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.
resetCaution
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.
| Mistake | Symptom | Solution |
|---|---|---|
Log at error level | No trace when fzf acts strange | Set FZF_LOG_LEVEL=debug and read /tmp/fzf-debug.log |
$TERM is dumb or xterm | Colors don't appear | Use a terminal whose TERM supports 256 colors |
| Washed-out colors in tmux | Limited to 16 colors | default-terminal "tmux-256color" + terminal-overrides ",*:Tc" |
| Silent preview | Preview command errors or exits with code 1 | Run it manually, add || true, check command -v |
| Exit codes 127/126 | A become command not found/denied | Fix the command in the become action, not fzf |
Ctrl+T doesn't work | Shell integration not loaded | eval "$(fzf --zsh)" in the right file, open a new shell |
Ctrl+R opens something else | Conflict with another plugin | Call bindkey '^R' fzf-history-widget last |
| Ghost characters in Zellij | Leftover characters while moving | Upgrade fzf to 0.74.1+ (CHA fix) |
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.