Most of a developer’s day is spent locating things. A file whose name you half-remember. The command you typed last Tuesday. The branch you were on before the meeting. The process squatting on port 3000. The terminal has always had answers to these questions, but the traditional answers — tab completion, linear history search, find with a wall of flags — all tax the same scarce resource, your attention.
A small set of modern command-line utilities has quietly become standard equipment because they remove that tax. fzf turns any list into an interactive search. ripgrep makes project-wide search instant. zoxide learns which directories you actually use. fnm makes switching Node versions a non-event. None of them takes more than an evening to install and internalize, and each starts paying for itself within the week — not because they do anything exotic, but because they smooth out interactions you repeat dozens of times a day.
This post is a tour of those four tools, the philosophy that ties them together, and a few recipes for composing them into a workflow that feels custom-built — because it is.
fzf: Stop Typing Exact Names
fzf is a command-line fuzzy finder, and it’s the tool most likely to change how your terminal feels. Give it a list on standard input and it opens an interactive search box: type a few characters of what you want, and the list narrows to matches in any order. Press Enter and your selection prints to standard output.
The killer feature is the shell integration. Load the key-binding script that ships with fzf and three shortcuts appear. Ctrl-R replaces the shell’s linear history search with a fuzzy one — type git re and watch every git revert and git rebase you’ve run in months scroll into view. Ctrl-T fuzzy-finds a file and inserts its path at your cursor, so you stop typing long paths by hand. Alt-C fuzzy-finds a directory and jumps into it. All three work in bash, zsh, and fish.
Even without the key bindings, fzf is a universal filter for any line-oriented output:
# fuzzy-find a file under the current directory, with a live preview
fzf --preview 'head -50 {}'
# filter any list — here, globally installed npm packages
npm ls -g --depth=0 | fzf
# find that long command you typed yesterday
history | fzf
If you take one idea from this section, make it this: fzf doesn’t care where the list comes from. Package lists, git branches, docker containers, SSH hostnames, your notes directory — if a command can print it, fzf can search it.
ripgrep: Search That Respects Your Project
ripgrep (the binary is rg) is a recursive search tool built for speed, but the first thing you’ll notice isn’t raw throughput — it’s that rg respects your project. By default it skips everything your version control ignores, so searching a JavaScript or Go repository doesn’t drown you in node_modules or vendor hits, and it steps over binary files without being asked. That single default eliminates half the flags you’d otherwise pass to grep.
The -t flag filters by file type, which covers most day-to-day searching:
# every TODO in Go files, with line numbers
rg -t go TODO
# case-insensitive search for a function, only in src/
rg -i 'renderheader' src/
# regex: find calls that render any kind of header
rg 'render[A-Z][a-z]+Header' -t go
That last example is a plain regular expression: character classes, quantifiers, anchors. rg uses a modern regex engine, so patterns behave the way you’d expect from any contemporary language. Two more flags cover most daily needs: -i for case-insensitive matching, and -g '!dist' to exclude a glob pattern when something ignored slips through.
One more trick that pays off immediately: rg --files prints every file it would search, one per line. Pipe that into fzf and you have a project-aware file finder that already skips everything gitignored — the two tools are better together than either is alone.
zoxide: A cd That Learns Your Habits
zoxide is a drop-in upgrade for cd that learns from behavior. Every time you change directories, zoxide records the path. Its own command, z, then jumps to the best match for whatever keywords you type, ranked by a blend of how often and how recently you visited each location.
# visit a deep directory once, the regular way
cd ~/projects/payment-service/api
# next time, jump there by keyword — matches are ranked
# by frequency and recency
z pay api
# not sure which match you want? pick interactively
zi pay
The first cd is the training data. After that, z pay api beats seventeen tab-presses every single time. And zi opens the same ranking inside fzf, so when your keywords match three similar service directories, you see them side by side before committing.
fnm: Node Versions Without the Wait
fnm is a Node version manager built as a single fast binary. The pitch is simple: installing and switching versions is quick enough that you stop noticing it. fnm install finishes in seconds, and fnm use switches instantly because there’s no wrapper script shimming every executable on your PATH.
# install two Node versions side by side
fnm install 20
fnm install 22
# switch for this shell, and set the global default
fnm use 22
fnm default 22
The part that changes daily behavior is the shell hook. Once fnm’s environment is wired into your shell’s startup file — the README has the exact line for bash, zsh, and fish — it watches for .nvmrc and .node-version files and switches Node automatically as you move between projects. cd into a repo pinned to Node 20 and that’s what you get; cd out and your default comes back. No manual switching, no remembering, no CI failures caused by your laptop disagreeing with the project.
The Philosophy: Small Tools That Compose
What ties these tools together isn’t speed, though they’re all fast. It’s that each does one job, speaks plain text on stdin and stdout, and gets out of the way. That’s the old Unix idea, and it’s why these utilities compose instead of merely coexisting. fzf is the clearest example: it’s a generic interactive filter, so it upgrades everything downstream of a pipe. rg --files | fzf is a better file finder than either tool alone. git log --oneline | fzf is a commit browser you wrote in five seconds with zero code.
There’s a compounding effect at work. A monolithic tool — an IDE, a GUI client — gives you the features its authors imagined, and those are often great. Composable tools give you the interactions nobody shipped, because you assemble them yourself, one pipe at a time. The cost is that nobody hands you the integrations; you build the ones you want. That’s the trade, and for interactions you perform many times a day, it’s usually worth making.
Building Your Own Workflow
Here’s what that looks like in practice. Checking out branches is the classic case: you know the branch exists, you know roughly what it’s called, and typing git checkout plus an exact name is pure friction.
# check out a branch by fuzzy-picking it
git branch --format='%(refname:short)' | fzf | xargs git checkout
# same trick for skimming recent commits
git log --oneline -20 | fzf
The git branch invocation uses a custom format to strip the decorations git adds by default, so clean names flow through fzf straight into xargs, which hands your selection to git checkout as a plain argument — three standard tools, zero glue code.
Port killing and container cleanup follow the same shape. For ports, the honest version is two steps:
# what is listening on port 3000? note the PID in the output
lsof -nP -iTCP:3000 -sTCP:LISTEN
# kill it by PID
kill -9 PUT-THE-PID-HERE
# stop a Docker container you pick interactively
docker ps --format '{{.ID}} {{.Names}}' | fzf | cut -d' ' -f1 | xargs docker stop
You’d normally collapse the port kill into a single line that captures the PID programmatically, but the two-step form keeps each command explicit and copy-pasteable. The Docker line repeats the branch-checkout pattern: format the output into clean columns, let fzf narrow it, cut out the ID, feed it to the command that acts. Once that shape is familiar — format, filter, extract, act — you’ll apply it everywhere, from switching kubectl contexts to picking a log file to tail.
When a GUI Still Wins
None of this is terminal absolutism. Some interactions are genuinely better in a GUI, and pretending otherwise wastes time:
- Browsing visual assets — image folders, design files, PDFs — where you evaluate by looking, not by naming.
- Three-way merge resolution, where side-by-side, syntax-highlighted panes beat any terminal diff.
- Profiling views like flame graphs, which are fundamentally interactive graphics.
- Anything you do rarely enough that muscle memory never forms — a GUI’s discoverability wins when there’s nothing to remember.
These tools win on repetition. The hundredth time you jump to a directory or pick a branch, the two seconds you save is real money. The first time, learning cost is all you notice. Match the interface to the frequency.
Wrapping Up
Every tool in this post is a few megabytes, installs in minutes, and replaces a habit you already have with a slightly better one. That’s the whole trick: no migration, no rewrite, just small frictions removed from loops you run dozens of times a day. The compounding is the point — a couple of seconds saved a hundred times a day adds up to most of an hour a week, and unlike most optimizations, this one makes the work feel lighter rather than just measuring faster.
If you’re picking a starting point, start with fzf. The Ctrl-R upgrade alone converts most people within a day, and the pipe-into-anything habit follows on its own. ripgrep, zoxide, and fnm each take an evening. A week later you’ll wonder how the terminal ever worked the old way.