I installed fzf years ago, used it for Ctrl-R history search, and called it done. For a long time that was the whole relationship. Then at some point in the last year I noticed that almost every interactive thing I do in a shell now goes through it, and the difference in how my hands move is large enough to be worth writing down.
The pitch is boring on paper. fzf reads lines on stdin, lets you fuzzy-filter them interactively, and prints the ones you pick to stdout. That's it. It is a filter. The reason it ends up everywhere is that "a list of lines you want to narrow down and choose from" describes an enormous fraction of what you do at a terminal, and once you have a fast, consistent way to do that, you stop writing grep | head | copy-paste the filename by hand.
the three bindings that earn their keep
Out of the box, the shell integration gives you three things, and these alone are most of the value.
Ctrl-R replaces the default history search. The default reverse-i-search is fine until you can't quite remember the command, at which point it's useless. With fzf you type fragments in any order. I'll type docker prune and it finds docker system prune -af --volumes from three weeks ago. No prefix matching, no cycling through near-misses one at a time.
Ctrl-T pastes a file path into the current command line. This sounds trivial and it changed how I write commands. I start typing vim then hit Ctrl-T, fuzzy-find the file from anywhere in the tree, and it drops the path in. I almost never type a path by hand any more.
Alt-C cd's into a subdirectory you pick interactively. Same idea, applied to navigation. I stopped maintaining a mental map of directory depth.
The thing they share is that they're all the same muscle: summon a list, type fragments, hit enter. Because the interaction is identical everywhere, it stops being a tool you think about and becomes a reflex.
making it find files faster
The default file-walking is fine on small trees and slow on large ones, because it shells out to find. If you have fd installed, point fzf at it and the difference on a big monorepo is the difference between instant and a noticeable pause. I set this in my shell rc:
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
export FZF_ALT_C_COMMAND='fd --type d --hidden --follow --exclude .git'
fd respects .gitignore by default, so the list you fuzzy-match against is already the list you care about: no node_modules, no build artefacts, no thousand-deep vendored trees drowning the signal. That last point matters more than the raw speed. A fuzzy finder is only as good as the haystack you feed it, and the cleanest haystack is usually "the files git would track".
the functions I actually wrote
Beyond the built-ins, fzf is at its best wrapped in tiny functions. These are not clever. They're just the same pipe a list in, do something with the choice shape, applied to the things I do twenty times a day.
Killing a process without hunting for the PID:
fkill() {
local pid
pid=$(ps -ef | sed 1d | fzf -m | awk '{print $2}')
[ -n "$pid" ] && echo "$pid" | xargs kill -"${1:-15}"
}
-m enables multi-select, so I can tab-mark several stuck processes and send them all the same signal. Defaulting to 15 (SIGTERM) and letting me pass 9 as an argument when something deserves it.
Checking out a branch without typing it:
fco() {
local branch
branch=$(git branch --all | grep -v HEAD | fzf | sed 's/.* //;s#remotes/[^/]*/##')
[ -n "$branch" ] && git checkout "$branch"
}
I work in repos with enough branches that tab-completion is a slog and remembering the exact name is hopeless. This turns "which branch was that ticket on" from a git branch | grep dance into three keystrokes and a fragment.
And the one I'd miss most, opening a file by searching its contents rather than its name:
frg() {
local file
file=$(rg --line-number --no-heading "${1:-.}" | fzf | cut -d: -f1)
[ -n "$file" ] && "${EDITOR:-vim}" "$file"
}
rg (ripgrep) does the searching, fzf does the choosing, and I land in the editor on the file I wanted. It's a poor person's "go to definition" that works in any language, any project, with no language server to configure.
the preview window
The single setting that made fzf feel like a proper tool rather than a list picker is the preview window. fzf can run a command for the currently-highlighted line and show its output in a pane. For files, that means seeing the contents before you commit to opening them:
export FZF_CTRL_T_OPTS="--preview 'bat --color=always --style=numbers {} 2>/dev/null || cat {}'"
bat gives syntax highlighting and falls back to cat when it isn't there. Now Ctrl-T is not just "find a path", it's "browse the tree and read files as you go". I catch the wrong file before I open it, which is a small thing that adds up.
why this stuck when other tools didn't
I've tried a lot of terminal productivity kit over the years, and most of it gets uninstalled within a fortnight because the cognitive cost of remembering it outweighs the saving. fzf stuck because it has exactly one idea, and that idea composes with everything I already do. I didn't have to learn a tool so much as learn to pipe things into it. Every time I catch myself about to grep a list and copy a result out by hand, that's now a function instead, and the function takes less time to write than the manual version takes to run twice.
If you only take one thing from this: install it, set FZF_DEFAULT_COMMAND to fd, turn on the preview window, and live with the three default bindings for a week. The functions will write themselves once your hands expect the list to always be a fuzzy-find away.