2026-08-28
entr and fatrace get all the attention, but they're the wrong shape for a lot of real jobs. entr blocks a terminal and re-runs a whole command list on every touch. fatrace observes but does nothing. What you actually want is cron for filesystem events: a daemon, per-user tables, persistent across reboots, one line per rule, with the full inotify event mask and event details passed as substitutions. That's incron, written by Lukáš Jelínek around 2006 and still shipping in Debian, Ubuntu, and EPEL as incron.
The interface deliberately apes cron. There's incrontab -e per user, /etc/incron.d/* for packages, and an incrond daemon under systemd. Each line has three fields: path, event mask, command with substitutions.
# SFTP drop box: process each fully-written upload exactly once
/srv/incoming IN_CLOSE_WRITE,IN_MOVED_TO /usr/local/bin/ingest "$@/$#"
# Reload nginx after any config change, but only if the config parses
/etc/nginx/conf.d IN_CLOSE_WRITE,IN_DELETE /bin/sh -c 'nginx -t && nginx -s reload'
# Kick off a rebuild when the source tree changes, no self-loops
/home/build/src IN_MODIFY,IN_NO_LOOP /usr/local/bin/rebuild.sh
The substitutions are the point:
$@ — the watched path$# — the filename inside it that triggered$% — the event flags as text (IN_CLOSE_WRITE)$& — the event flags as the raw inotify bitmaskSo one incrontab line handles any new file in the directory, with the exact filename and the exact event, without you writing a shell loop around inotifywait.
Why it beats the obvious alternatives. A systemd .path unit only understands PathChanged, PathModified, PathExists, PathExistsGlob, DirectoryNotEmpty, and it needs a paired .service unit. incron gives you the whole inotify vocabulary — IN_CLOSE_WRITE, IN_MOVED_TO, IN_ATTRIB, IN_DELETE_SELF — in a one-liner. A cron job polling find -newer at one-minute granularity misses bursts and hammers the disk; incron is event-driven and immediate.
The traps the docs bury. These are the ones that bite people:
IN_MODIFY fires many times during a big write. Use IN_CLOSE_WRITE for "the file is done."IN_ONLYDIR and a small wrapper.IN_NO_LOOP flag — incron suspends the watch while the handler runs.fs.inotify.max_user_watches. Bump it in /etc/sysctl.d/ before pointing incron at a large tree./etc/incron.allow, mirroring /etc/cron.allow.Where it shines. SFTP intake boxes where you want per-file processing without a polling loop. Reloading a daemon whenever its config directory changes. Kicking off rsync or restic after a directory is quiet. Auto-importing dropped photos into a library. Triggering a lint run when a shared mount changes upstream. Anywhere you would have written a while inotifywait ...; do ... done as a systemd service — that's an incrontab line instead.
incron is what you actually wanted when you reached for entr, cron, or a systemd path unit: a persistent daemon with a cron-shaped table that fires shell commands on the full inotify event set, with the triggering filename handed to your command for free.
