CLI tool
lincheney/fzf-tab-completion avatar
lincheney/fzf-tab-completion

fzf-tab-completion: fuzzy completion that works with the shell's own mechanism

Tab completion using fzf

866 stars49 forksShellGPL-3.0

At a glance

What is it?
fzf-tab-completion puts fzf in front of the completion system your shell already has, rather than replacing it. It works in zsh, bash, GNU readline applications, Node and Python, and the interesting part is the configuration surface: per-command preview scripts, post-completion keybindings, display-string colouring and a longest-common-prefix mode that makes it behave more like vanilla tab completion.
Who is it for?
Adopt fzf-tab-completion if you already use fzf and want it to cover shell completion properly, including the completion functions your shell ships with rather than only file names. Do not adopt it if you are starting from scratch with a shell setup, since it assumes fzf is installed and configured, and expect to read the zstyle documentation before the first week is over.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 84 days ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The design decision: layering, not replacing

fzf-tab-completion gives you tab completion using fzf in zsh, bash, GNU readline apps such as the interactive Python or PHP with the -a flag, and it also ships scripts for Node and Python.

What makes it distinct is stated in the second paragraph of the README, and it is the whole reason to use this project rather than fzf's own completion support. fzf has its own fuzzy completion for bash and zsh, but that approach creates a new mechanism. This one works with the existing completion mechanisms instead.

The distinction is practical rather than philosophical. A shell's completion system knows a great deal: that `git checkout` completes branches, that `ssh` completes hostnames from your config, that `docker` completes image names, that a path argument should expand directories differently from a flag argument. fzf's own completion works from a list of candidate strings you supply. This project takes the list the shell already computed, its display strings and its candidate values, and puts fzf in front of it as a selection interface.

So the completion candidates stay correct because they come from the shell's own knowledge, and what fzf adds is the ability to narrow a long list by typing rather than by cycling through it. For a directory with a hundred entries or a git branch list of two hundred, that is the difference between a usable and an unusable interface.

The repository layout follows the four supported shells directly, with zsh/, bash/, readline/, node/ and python/ directories at the root next to the README, the LICENSE and an example.svg recording the behaviour.

Installing it across four different completion systems

The installation steps assume you already have fzf, because the README says you need to install fzf first and links to its installation instructions.

For macOS there is an extra step. The README says you may need to install some additional things, giving `brew install gawk grep gnu-sed coreutils` as the example, because the scripts rely on GNU versions of those tools rather than the BSD ones macOS ships.

Then you clone the repository, with `git clone https://github.com/lincheney/fzf-tab-completion ...`, and the README notes that you can also choose to download only the scripts you need, which matters because the four directories are independent of each other.

The zsh setup is two lines in your ~/.zshrc:

bash
source /path/to/fzf-tab-completion/zsh/fzf-zsh-completion.sh
bindkey '^I' fzf_completion

The bindkey line is optional if you have already enabled fzf's own zsh completion, since the two mechanisms would otherwise both claim the tab key.

The bash setup is the same shape in ~/.bashrc, with a different binding call:

bash
source /path/to/fzf-tab-completion/bash/fzf-bash-completion.sh
bind -x '"\t": fzf_bash_completion'

The README adds a fork in the road for bash users. If your bash is dynamically linked against readline, checked with `LD_PRELOAD= ldd $(which bash)`, you may prefer the readline method instead, and the phrasing leaves it open whether that is a good idea or not.

Both shells carry the same caveat about `**`-style triggers. This project does not provide them, so you need to enable fzf's own completion as well if you want that behaviour.

Three environment variables carry through from fzf unchanged, following the same convention as fzf's own completion: $FZF_TMUX_HEIGHT, $FZF_COMPLETION_OPTS and $FZF_DEFAULT_OPTS.

The zstyle configuration surface, and why it is the interesting part

Most of the README is about zsh styles, and the reason is that zstyle gives you a way to configure behaviour per command, which the environment variables cannot do.

The first thing to understand is a set of flags the README asks you not to change: -n, --nth, --with-nth and -d. The reason is structural. This project displays the shell's display string, which is often longer than the actual candidate value, and then selects from the candidate values. The flags in question control how fzf matches and displays, and they are set by this project because the two representations have to line up for the selection to be correct.

The second style is fzf-search-display. By default, display strings are shown but cannot be searched in fzf. That is configurable per command:

bash
# only for git
zstyle ':completion:*:*:git:*' fzf-search-display true
# or for everything
zstyle ':completion:*' fzf-search-display true

The third is fzf-completion-opts, which lets you set different fzf options depending on the command being completed, unlike the global environment variables. The README says this is most useful for changing the --preview option, and gives a series of examples. A basic file preview for ls:

bash
# basic file preview for ls (you can replace with something more sophisticated than head)
zstyle ':completion::*:ls::*' fzf-completion-opts --preview='eval head {1}'

Then a preview for environment variables, with a note that it only works for exported variables and that eval is applied twice, once to unescape the string and once to expand the variable:

bash
# preview when completing env vars (note: only works for exported variables)
# eval twice, first to unescape the string, second to expand the $variable
zstyle ':completion::*:(-command-|-parameter-|-brace-parameter-|export|unset|expand):*' fzf-completion-opts --preview='eval eval echo {1}'

And then the git cases, showing a status preview for git add and a diff or log preview for other subcommands:

bash
# preview a `git status` when completing git add
zstyle ':completion::*:git::git,add,*' fzf-completion-opts --preview='git -c color.status=always status --short'

One detail catches people out. The README warns that {1} and {+1} come through shell-escaped, so you need to unescape them, using eval or printf %b. That is why the env var example evaluates twice.

The final style is fzf-completion-secondary-color, which controls the colour of the display string and the input prefix, the parts of the strings that are not searchable. By default those are highlighted with an escape sequence that should come out as light grey, and you can change it:

bash
# make it red instead
zstyle ':completion:*' fzf-completion-secondary-color red

It accepts anything usable in the zsh prompt escape, including names like red and hex sequences, and setting it to an empty string removes the colour entirely.

Keybindings that run after fzf closes

The most unusual capability is the ability to run shell commands after fzf has closed, and it is configured through the fzf-completion-keybindings style.

The format is KEY:SCRIPT. When the key is pressed, fzf exits and the zsh script runs. If the keybind is given in the form KEY:accept:SCRIPT, then the selected matches are also completed before the script runs. The key can be any valid fzf key.

The example assigns three behaviours that show the range:

bash
# press ctrl-r to repeat completion *without* accepting i.e. reload the completion
# press right to accept the completion and retrigger it
# press alt-enter to accept the completion and run it
keys=(
    ctrl-r:'repeat-fzf-completion'
    right:accept:'repeat-fzf-completion'
    alt-enter:accept:'zle accept-line'
)

zstyle ':completion:*' fzf-completion-keybindings "${keys[@]}"
# also accept and retrigger completion when pressing / when completing cd
zstyle ':completion::*:cd:*' fzf-completion-keybindings "${keys[@]}" /:accept:'repeat-fzf-completion'

The repeat-fzf-completion function is what makes this work, and it can be called from within the script to retrigger fzf completion. That is how you get a cycle: accept, keep the popup open, pick again.

No keybinds are configured by default, which is the right default for a behaviour this unusual. The README also notes that you can still use the normal --bind options in $FZF_COMPLETION_OPTS if you want fzf-specific actions or do not need to run zsh commands.

A separate, smaller section covers the tiebreak behaviour. The default fzf tiebreak setting is line, which prefers the line with the shorter length, and the README points out that the length of the zsh display strings may skew the ordering of the results even though they are not part of the match. The fix is to add the fzf flag --tiebreak=chunk to $FZF_COMPLETION_OPTS, which ties on the matched chunk rather than the whole line.

The bash mode that feels like vanilla tab completion

The bash side of the README is much thinner than the zsh side, and one feature in it is worth pulling out.

By default, fzf is always shown whenever there are at least two matches. The bash mode offers a different behaviour, described as a more vanilla tab completion experience where it attempts to complete the longest common prefix before showing matches in fzf.

That is controlled by two environment variables, and the README gives the first as FZF_COMPLETION_AUTO_COMMON_PREFIX=true, which completes the common prefix if it is also a match. The second variable is named in the README but the text is cut off in the available text, so the flag that disables it cannot be confirmed from the documentation.

This single behaviour is what decides whether the bash integration feels natural or feels intrusive. On zsh, fzf appearing on every ambiguous completion is the expected behaviour of the project and most people who install it want that. On bash, where the muscle memory is tab-tab to see the list, a popup every time is a bigger change, and the longest-common-prefix mode lets you keep the old feel in the common case while still offering the fuzzy list when it matters.

The bash prompt is also configurable through FZF_TAB_COMPLETION_PROMPT, which sets the prefix and defaults to a simple greater-than-and-space prompt. The README's example for changing it uses a heavier character.

There is one more tmux-related detail on the zsh side. $FZF_TMUX_OPTS is respected in the same way as in fzf itself, but you must have fzf's keybindings enabled as well for it to apply.

Limits and the cases where it is the wrong tool

The limits are mostly consequences of the design, and the README is reasonably forthcoming about them.

The first is the dependency on fzf being set up already. This is a completion front end, not a search tool. If you want fuzzy finding, install fzf; this project assumes it, and a large part of the configuration surface is expressed in fzf's own options and key names.

The second is the constraint on fzf flags. The README asks you to avoid -n, --nth, --with-nth and -d. That is a real limitation on what you can do, and it exists because the display string and the candidate value are different things and fzf has to be told to match against one while showing the other. A user who wants a different matching behaviour has to work around the project's own configuration rather than set the flag directly.

The third is shell coverage. There are scripts for zsh, bash, GNU readline applications, Node and Python, and the README's advice for bash users on a readline-linked build is to consider the readline method instead. Anything else, a fish shell for instance, is not covered by this repository, and the zstyle configuration model is specific to zsh, so the bash side is permanently the poorer of the two.

The fourth is that the `**`-style triggers do not work. The README says so twice, once for zsh and once for bash, and the fix is to enable fzf's own completion as well, which means two completion systems running side by side. That is workable but it is not elegant, and it is a reminder that the two projects overlap in a way that has not been reconciled.

The fifth is the display string limitation. The README describes the fact that display strings are shown but not searched by default, and the fix is a zstyle. On zsh that is a clean answer. On bash there is no equivalent, so a bash user gets display strings that cannot be searched, which is confusing until you know why.

This project against fzf's own completion and against a plain completion menu

Two comparisons put it in context.

The first is against fzf's own completion for bash and zsh, which the README distinguishes itself. fzf's approach creates a new mechanism: it takes the shell's candidate list, but the matching and the display are its own, and the shell's completion functions are invoked through a separate path. This project's approach is to keep the shell's mechanism intact and only replace the selection interface, so the display strings, the candidate values and the completion function stay coupled the way the shell intended.

The practical difference shows up in the configuration. This project's zstyle options are scoped by command and by completion context, so you can have a file preview for ls, a status preview for git add and a different preview for environment variables, all at once. fzf's own completion does not have that per-command preview mechanism in the same form. If you want to look at what you are about to select before selecting it, this project gives you a way to build that.

The second comparison is against the plain completion menu that every shell ships with. The stock menu cycles through candidates with repeated tab presses, and it is fast when there are three candidates and useless when there are three hundred. The long-standing answer to that has been the plain text menu, with the numbers next to the candidates and a number to type.

fzf is a better version of that idea, because fuzzy matching means you can type a fragment and find the one you want. This project is that idea applied to the candidate list your shell already computed, which is the version that works with completion functions rather than only with files. The cost is that the popup appears on every ambiguous completion, which is the change that makes some people prefer the stock behaviour and others cannot live without the popup.

Editorial conclusion

Adopt fzf-tab-completion if you already use fzf and want it to cover shell completion properly, including the completion functions your shell ships with rather than only file names. Do not adopt it if you are starting from scratch with a shell setup, since it assumes fzf is installed and configured, and expect to read the zstyle documentation before the first week is over. Verify first that the fzf flags the README warns against are not set in your own options, because the display strings depend on how fzf matches, and check that the shell you use is one of the four the repository ships scripts for.

Frequently asked questions

How do I install fzf-tab-completion in zsh and bash?

Install fzf first, clone the repository, then source the shell script and bind the tab key. In zsh add the source line for zsh/fzf-zsh-completion.sh plus bindkey '^I' fzf_completion to your ~/.zshrc. In bash source bash/fzf-bash-completion.sh and add bind -x for the tab key to ~/.bashrc.

How do I add a preview window to the fzf completion popup?

Use the fzf-completion-opts zstyle, which allows different fzf options depending on the command being completed. The README's example sets a head preview for ls, and notes that the {1} placeholder arrives shell-escaped so you need eval or printf %b to unescape it.

Which fzf flags should I not change when using fzf-tab-completion?

The README says to avoid -n, --nth, --with-nth and -d, because this project sets them itself to keep the shell's display strings and the actual candidate values aligned. It also notes that the default fzf tiebreak of line can skew the ordering, and suggests adding --tiebreak=chunk to $FZF_COMPLETION_OPTS instead.

Official sources

  1. Issues
  2. License: GPL-3.0
  3. lincheney/fzf-tab-completion on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/lincheney-fzf-tab-completion.svg)](https://hysenlabs.com/projects/lincheney-fzf-tab-completion)

Was this useful?