2026-09-14
Every few years someone reinvents this: crawl a directory, hash every file, save the list, diff it later. The reinventions are usually a shell script wrapped around sha256sum and find, and they always miss something — mode bits, symlink targets, ownership, xattrs, or the fact that a file being missing matters as much as one being modified.
BSD solved this in 1989. It's called mtree, and on Linux you install it as mtree-netbsd (Debian/Ubuntu) or bsdmtree (via libarchive on Fedora/Arch). It writes a plain-text specification of a directory tree — one entry per file, with whichever keywords you asked for — and later verifies any tree against that spec.
The spec looks like this:
/set type=file uid=0 gid=0 mode=0644
. type=dir
hosts size=412 sha256digest=e3b0c44298fc1c14...
passwd size=1834 mode=0644 sha256digest=af5570f5a1810b7a...
shadow mode=0640 gid=42 sha256digest=6b3a55e0261b0304...
/set lines set defaults that following entries inherit — so the file is dense, not repetitive. Snapshot /etc:
mtree -c -K sha256digest,size,mode,uid,gid,link -p /etc > /var/backups/etc.mtree
Then, later — cron, post-deploy, after an intrusion scare — verify:
mtree -f /var/backups/etc.mtree -p /etc
Output tells you exactly what drifted:
ssh/sshd_config: sha256digest expected af5570f5... got 6b3a55e0...
ssh/sshd_config: modification time expected Sun Sep 14 03:12:04 2026 got Mon Sep 14 03:47:19 2026
./cron.d extra: mystery_job
./motd missing
Three categories in one pass: modified, extra, missing. That last one is where the homegrown find | sha256sum pipelines always fall over — they hash what's there, not what should be there.
Where it beats the alternatives:
diff, edit, check into git, or grep.find … -exec sha256sum: catches deletions, mode changes, ownership drift, symlink retargeting, and device-node changes — not just content edits./dev or SUID bits.The tricks nobody tells you about:
Compare two live directories without ever writing a spec to disk:
mtree -c -K sha256digest -p /srv/app-v1 | mtree -p /srv/app-v2
Reset a tree's permissions from a known-good spec (this is how FreeBSD lays down / on install):
mtree -U -f /etc/mtree/BSD.root.dist -p /
The -U flag doesn't just report drift — it repairs it, chmod'ing and chown'ing to match. Content it won't touch; ownership and modes it will.
Exclude noisy paths with a plain-file list (one glob per line):
mtree -c -X /root/exclude.list -p /var > /var/backups/var.mtree
Filesystem manifests as build artifacts: generate the spec at package time, ship it with the package, and let the installer verify the extraction. Debian's debsums is a weaker version of this idea; rpm -V is a stronger version welded into RPM. mtree does it as a standalone file, portable across any Unix.
The whole tool is one C binary, no dependencies, no state directory, output is human-readable, exit code is zero when clean and non-zero when drifted. That's about as Unixy as intrusion detection gets.
mtree gives you tamper-evident directory manifests in a plain-text format — with no daemon, no database, and honest handling of missing files — replacing whatever find | sha256sum script you were about to write.
