Skip to main content
Settings
Search
Appearance
Theme Mode
About
Jekyll v3.10.0
Environment Production
Last Build
2026-06-27 22:15 UTC
Current Environment Production
Build Time Jun 27, 22:15
Jekyll v3.10.0
Build env (JEKYLL_ENV) production
Quick Links
Page Location
Page Info
Layout article
Collection tools
Path _tools/fd-honest-review.md
URL /tools/fd-honest-review/
Date 2026-06-27
Theme Skin
SVG Backgrounds
Layer Opacity
0.6
0.04
0.08

fd: the honest review

Verdict: install it everywhere, but learn its two defaults before you trust it — because the same sanity that makes fd pleasant also makes it quietly hide files you came looking for. fd is a friendlier find: you type a pattern, it searches the current directory tree, and it does the obvious thing without seven flags and a -print0. We reach for it daily. We also got burned by it twice while writing this review, and both burns are in the box on purpose.

fd is free and open source (Apache-2.0 / MIT). We have no relationship with the project and nothing to sell. Like its sibling ripgrep, the dealbreaker here isn’t price or telemetry — it’s a couple of defaults that surprise anyone arriving from find. We’ll show you exactly where, with output we actually captured.

Install — and the first surprise is the name

brew install fd             # macOS
sudo apt install fd-find    # Debian/Ubuntu

On macOS and most distros the command is fd. On Debian and Ubuntu it is not — the name fd was already taken by another package, so apt ships the binary as fdfind. So the very first thing you do, copy the install line from the README, and:

$ fd --version
/bin/bash: line 41: fd: command not found
$ dpkg -L fd-find | grep bin
/usr/bin
/usr/lib/cargo/bin
/usr/lib/cargo/bin/fd
/usr/bin/fdfind

The command on your PATH is fdfind. (/usr/lib/cargo/bin/fd exists but isn’t on your PATH, which is its own little tease.) Every example in every blog post says fd. Yours says fdfind. The fix is one line. The bulletproof version is a symlink:

$ ln -s "$(which fdfind)" ~/.local/bin/fd
$ fd --version
fdfind 9.0.0

The lighter version is an alias in your ~/.bashrc — we tested that it does work once sourced into a shell:

$ source ~/.bashrc   # contains: alias fd=fdfind
$ fd --version
fdfind 9.0.0

One honest caveat on the alias: aliases only exist in interactive shells, so a script that calls fd will still hit command not found. Scripts should use fdfind (or the symlink) outright. For the rest of this review we ran the real binary, so the prompts below say fdfind.

Why you’d switch from find

Everything below ran against a throwaway git repo we built for the occasion — a src/ dir, some logs, a node_modules/, a .gitignore, and a couple of dotfiles. First, the headline contrast. Find what’s named like “app”:

$ fdfind app
src/App.test.js
src/app.js

$ find . -name "*app*"
./src/app.js
./.git/hooks/pre-applypatch.sample
./.git/hooks/applypatch-msg.sample

Three things happened in that one comparison, and they’re the whole pitch:

  1. fd is smart-case. A lowercase app matched App.test.js and app.js. find -name "*app*" would have needed -iname to catch the capital.
  2. You don’t wrap the pattern in *…*. fd does a substring/regex match by default; find -name wants explicit globs.
  3. fd skipped the .git noise. find dredged up two .git/hooks/*applypatch* sample files nobody asked for. fd ignores hidden directories by default — which is wonderful, until it isn’t (hold that thought).

A few more dailies, all real:

$ fdfind -e md            # filter by extension
README.md
notes.md

$ fdfind -g "*.test.js"   # glob instead of regex, with -g
src/App.test.js

$ fdfind -e md -x wc -c {}   # run a command per result; {} is the file
0 ./notes.md
0 ./README.md

That -x is find -exec without the \; ceremony, and it parallelizes across results for free. For most “find some files, do a thing to each” jobs, this is the whole tool.

The two surprises in the box

Here’s where the same sanity that made fd skip the .git noise turns around and bites you.

Surprise 1: fd obeys your .gitignore. Our repo’s .gitignore lists *.log and node_modules/. Watch fd pretend those files don’t exist:

$ fdfind log
logs/

$ fdfind -I log     # -I / --no-ignore: stop respecting .gitignore
logs/
logs/error.log
logs/server.log

The first command found the logs/ directory but not the two .log files inside it — they’re gitignored, so fd filtered them out silently. No warning, no “2 files hidden” footer. If you’ve ever run fd something in a repo, gotten nothing, and sworn the file was right there: it probably was, and it was in .gitignore. The fix is -I (or -u, which we’ll get to).

Surprise 2: fd skips hidden files. Same silent treatment for dotfiles:

$ fdfind env
$ fdfind -H env     # -H / --hidden
.env

The first command printed nothing for .env. Searching for your .env, your .config/, your .github/ workflows? You need -H. Want both behaviors off at once — show me genuinely everything, like find does — that’s -u (--unrestricted, i.e. -H -I together).

These two defaults are the right call: 95% of the time you want to search your actual source, not node_modules and build junk and .git. But the cost is a class of bug where fd returns an honest, confident, wrong-looking empty result, and the only tell is that you forgot which mode you’re in. The number tells the story — on our little repo:

$ fdfind --type f | wc -l    # files fd shows by default
5
$ find . -type f | wc -l     # files find shows
30

fd showed 5 of 30 files. The other 25 are .git internals, the gitignored logs, and node_modules — exactly the noise you usually want gone, and exactly the files you’ll go looking for the one day you need them.

Where plain find still wins

fd covers the common cases beautifully and then hits a ceiling. find’s test-predicate language is genuinely more expressive for the gnarly stuff:

$ find . -name "*.log" -mmin -60      # changed in the last 60 minutes
./logs/error.log
./logs/server.log

fd can do recency — fdfind --changed-within 1h -e log -I returns the same two files — but the moment you want “files modified more than 30 days ago, owned by root, with the setuid bit set, and -delete them,” you’re back in find, where that’s a single (if cryptic) command. fd deliberately doesn’t grow a -perm/-newer/boolean-expression grammar. That’s a feature for your sanity and a wall for your edge cases. Keep find in your head for the 5%; let fd have the 95%.

What it costs and the free alternative

It costs nothing — open source, no account, no telemetry, no paid tier. The free alternative is the one already on your machine: find. The honest trade is keystrokes and defaults versus raw predicate power. If you only ever run two find commands a month, fd is a nicety, not a necessity. If you search trees all day, the smart-case, the regex-by-default, the parallel -x, and the auto-skipping of .git/node_modules add up fast — provided you’ve internalized -H, -I, and -u.

(fd also pairs naturally with fzf: set FZF_DEFAULT_COMMAND='fdfind --type f' and fuzzy-finding inherits fd’s sane defaults — same trade, same caveats, fewer junk results in the picker.)

What made us close the tab

Nothing — fd is staying on every machine. The honest caveats, in the order they’ll bite you:

  • The name isn’t fd on Debian/Ubuntu. It’s fdfind. Symlink or alias it on day one or every tutorial lies to you.
  • It hides gitignored files (.gitignore is respected by default). An empty result in a repo usually means “it’s ignored,” not “it’s gone.” Add -I.
  • It hides hidden files. Searching for dotfiles needs -H. The combined “show me everything” switch is -u.

When it goes wrong: if fd returns nothing and you know the file exists, you’re almost always fighting a default, not a bug. Run it again as fdfind -u <pattern> — unrestricted mode turns off both the gitignore and hidden-file filters at once. If it shows up now, you’ve found your culprit; add back the narrower -H or -I you actually needed. Learn those three flags and fd stops surprising you and gets out of the way.