fdupes Refuses To2026-08-16
Every few years someone reinvents fdupes. rmlint went the other way: it kept the hashing tight, then bolted on every deduplication and filesystem-cleanup task you actually wanted fdupes to do. It's been maintained since 2010, ships in Debian and Fedora, and its killer feature is that it never deletes anything itself — it writes you a shell script you can read, edit, and run.
The default run is deceptively boring:
$ rmlint ~/Downloads
# Traversing … Preprocessing … Matching …
# Duplicate(s): 412
# Total lint size: 3.8 GB
# Wrote sh script to: /home/shaun/rmlint.sh
Now look at rmlint.sh. It has entries like:
original_cmd '/home/shaun/Downloads/iso/debian-12.iso' # original
remove_cmd '/home/shaun/Downloads/old/debian-12.iso' 'ab12…' # duplicate
Nothing has happened yet. You edit the script (comment out things you want to keep), then run it. fdupes -d gives you an interactive prompt that scales to about six files before you rage-quit; rmlint's script scales to hundreds of thousands.
The handling modes are where it earns its keep. Instead of deleting duplicates, tell it what to do with them:
# Replace duplicates with reflinks (instant, zero extra bytes on btrfs/xfs/bcachefs)
$ rmlint -c sh:reflink /mnt/photos
# Or hardlinks (works on ext4 too, but breaks if either "copy" is edited)
$ rmlint -c sh:hardlink /var/backups
# Or symlinks with a relative path
$ rmlint -c sh:symlink /srv/media
The reflink mode is the one people don't realise exists. On btrfs or a modern XFS, cp --reflink makes two directory entries point at the same extents until one is written to. rmlint will convert your ten copies of the same Steam library into ten reflinked entries in a few minutes, and every one still behaves like an independent file.
It also finds lint that isn't duplicates at all:
$ rmlint --types="emptyfiles,emptydirs,badlinks,badids,nonstripped" ~
That single invocation reports empty files, empty directories, broken symlinks, files owned by UIDs that no longer exist in /etc/passwd, and unstripped binaries. Try composing that pipeline out of find, file, and getent and you'll be there all afternoon.
The hashing pipeline is worth knowing about. rmlint uses progressive matching: first size, then a cheap SHA1 of the first few KB, then a full BLAKE2 (or xxhash, spookyhash, murmur — -a picks). Two files that differ in byte 500 never get fully hashed. On a spinning disk it reads files in inode order to keep the head from thrashing. On SSDs it parallelises. This is why it will chew through a terabyte of duplicates faster than fdupes will chew through a hundred gigs.
Two flags worth remembering:
--xattr — cache checksums in extended attributes so a repeat run skips the hashing entirely// path — the "tag" syntax. Everything before // is originals, after is where duplicates get removed. rmlint ~/canonical // ~/inbox means "delete anything from inbox that already exists in canonical" — great for import workflows.The GUI (shredder) exists but is a distraction. Live in the shell script.
rmlint replaces fdupes, ad-hoc reflink scripts, and a dozen find incantations with one tool that outputs a reviewable shell script instead of trusting itself to delete your data.
