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:
fdis smart-case. A lowercaseappmatchedApp.test.jsandapp.js.find -name "*app*"would have needed-inameto catch the capital.- You don’t wrap the pattern in
*…*.fddoes a substring/regex match by default;find -namewants explicit globs. fdskipped the.gitnoise.finddredged up two.git/hooks/*applypatch*sample files nobody asked for.fdignores 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
fdon Debian/Ubuntu. It’sfdfind. Symlink or alias it on day one or every tutorial lies to you. - It hides gitignored files (
.gitignoreis 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.