Enabling fuzzy completion with the ** trigger followed by TAB to complete files, directories, processes, SSH hosts, environment variables, aliases, and commands, and building a custom completer for any data.

Episode 7 installed fzf into your shell via keybindings; episode 8 organized its configuration variables. Episode 9 opens up another side of the same integration script: fuzzy completion. If CTRL-T selects after you've typed the context, fuzzy completion works within the context — in the middle of the command you're typing. The simplest example: vim **<TAB>.
This feature is often called fzf's "killer feature" for one reason: it turns TAB, which previously only completed prefixes, into a full search engine. Instead of pressing TAB repeatedly to browse candidates, you type a few letters and the best result is selected directly.
**<TAB> TriggerHow to activate fuzzy completion: type two asterisks then press TAB:
vim **<TAB>Once **<TAB> is pressed, fzf replaces the command segment behind it with fuzzily filtered candidates. This differs from CTRL-T, which inserts a path as is; completion understands the command context and picks a matching candidate list.
Important
The default trigger is two asterisks (**), and it can be changed via the FZF_COMPLETION_TRIGGER variable — for example export FZF_COMPLETION_TRIGGER=';'. But remember: ** in bash/zsh is the globstar pattern. While fzf completion is active, the **<TAB> sequence is handled by the integration script; without integration, ** will be expanded by the shell as a normal glob.
The integration script provides completers for common contexts. The pattern is always the same — type a command, followed by **<TAB>:
| Context | Example | Candidates |
|---|---|---|
| File | vim **<TAB> | Files under the working directory |
| Directory | cd **<TAB> | Directories |
| Process | kill -9 **<TAB> | List of running processes |
| SSH host | ssh **<TAB> | Hosts from ~/.ssh/config & known_hosts |
| Environment variable | unset **<TAB> | Env variable names |
| Alias | unalias **<TAB> | Defined shell aliases |
| Command | man **<TAB> | Available command names |
cd **<TAB>This is the completion version of ALT-C: the candidates are directories, the result is a path ready for the command. For files, just change the command context — fzf guesses whether files or directories are needed from the command in front of it.
kill -9 **<TAB>A favorite example: typing kill -9 **<TAB> shows the process list with their PIDs, pick one fuzzily, and fzf inserts its PID — not the process name. No need to run ps aux first to find the PID.
ssh **<TAB>This completer reads ~/.ssh/config and known_hosts, so a host like web-prod-01 can be selected without typing its full name. This is invaluable if you manage many servers.
unset **<TAB>Each context has its own candidate logic: unset shows env vars, unalias shows aliases, man shows commands registered in $PATH. What differs is not the syntax — always **<TAB> — but what the integration script sends as candidates.
Tip
Don't memorize the list above. The way to think about it: anything that can be completed with a plain TAB can be completed fuzzily with **<TAB>. If the candidates that appear don't feel right, remember that FZF_COMPLETION_OPTS can tune the behavior of all completions at once — for example adding a preview: export FZF_COMPLETION_OPTS="--preview='head -50 {}'".
Two variables control completion behavior globally:
| Variable | Function | Default |
|---|---|---|
FZF_COMPLETION_TRIGGER | Trigger character | ** |
FZF_COMPLETION_OPTS | Default options for all completions | empty |
FZF_COMPLETION_OPTS works like FZF_DEFAULT_OPTS but specifically for completion invocations. You can add a preview, layout, or keybinding only for completion mode without affecting other fzf usage:
export FZF_COMPLETION_OPTS="--height=30% --preview='head -20 {}'"The full power of completion appears when you add candidates of your own data. The integration script defines a _fzf_complete helper that is called from a completer function named _fzf_complete_<command>.
The basic structure is the same for bash and zsh:
_fzf_complete_todo() {
_fzf_complete -- "$@" < <(
grep '^@' "$HOME/todo.txt"
)
}
complete -F _fzf_complete_todo -o bashdefault -o default todoWith the definitions above, typing todo **<TAB> shows all lines starting with @ from todo.txt, select one fuzzily, and it is inserted onto the command line. Let's break down the parts:
_fzf_complete_todo — the function name must follow the _fzf_complete_<command> pattern so it's called automatically._fzf_complete -- "$@" — calls the core helper; -- "$@" forwards the command-line arguments being typed (usable for --query, --multi, etc.).< <(...) — process substitution sends candidates to fzf's stdin without modifying the environment.complete -F (bash) / compdef (zsh) — registers the completer for the todo command.The candidates you send determine what can be selected — and also what is inserted. If you want to insert something different from what's displayed, use --accept-nth (episode 6) inside the _fzf_complete call:
_fzf_complete_kill_byservice() {
_fzf_complete --accept-nth=1 -- "$@" < <(
systemctl list-units --type=service --no-legend | awk '{print $1, $5}'
)
}
complete -F _fzf_complete_kill_byservice -o bashdefault -o default kill-byserviceHere the list shows nama.service deskripsi, but only the first field is inserted (--accept-nth=1) — a practical candidate for passing on to the next command.
Note
Custom completers live in your shell config, not in fzf. That's why they also follow the episode 8 principle: define them before the integration line is loaded, and reload your shell after changing them. For data that changes often, consider using find, rg, or sed inside the candidates so they're always fresh when **<TAB> is pressed.
Episode 9 brought fzf's "command-completing" side to life: the **<TAB> trigger for fuzzy completion, built-in completers for files, directories, processes, SSH hosts, env vars, aliases, and commands, global settings via FZF_COMPLETION_TRIGGER and FZF_COMPLETION_OPTS, and custom completers with the _fzf_complete helper.
The key takeaways:
**<TAB> in the middle of a command to trigger fuzzy completion.kill -9 **<TAB>, ssh **<TAB>, cd **<TAB>, unset **<TAB> are the most productive examples._fzf_complete_<command> + complete -F (bash) or compdef (zsh).In episode 10, we will build one of the features that makes fzf feel "alive": the preview window — a second pane that shows file contents, images, even command output in real time as you navigate the list. See you in episode 10!