Verdict: install it for the loop you rebuild by hand ten times a day — “run this every time I save” — and learn two things first, or you’ll stare at a watcher that isn’t watching what you think. entr reads a list of filenames on standard input and runs a command whenever any of them changes. That’s the whole tool. No config, no daemon, no plugin system. You pipe it a file list, you give it a command, and it gets out of the way. We reach for it whenever the job is “re-run the tests / rebuild the docs / restart the server on save,” which is most days. It also surprised us twice while we wrote this review, and both surprises are in the box on purpose.
entr is free and open source (ISC, by Eric Radman). We have no relationship with the project and nothing to sell. The catch here isn’t price or telemetry — it’s a design choice and a terminal quirk that ambush anyone expecting a “watch this folder” tool. We’ll show you exactly where, with output we captured on a fresh Ubuntu 24.04 box running entr 5.5.
Install
brew install entr # macOS
sudo apt install entr # Debian/Ubuntu
No rename tax this time — unlike fd shipping as fdfind or bat as batcat, the command on your PATH is plain entr. Run it with no input and it prints its whole usage in one line, which tells you almost everything:
$ entr
release: 5.5
usage: entr [-acdnprsz] utility [argument [/_] ...] < filenames
Read that < filenames at the end carefully. It is not a bug in the docs and it is the single most important thing about the tool: entr does not go find files. You hand it the list.
The pitch: rerun a command on save, in one line
The canonical incantation is <something that lists files> | entr <command>. Watch a shell script and re-run it every time it changes:
ls greet.sh | entr -p -s 'bash greet.sh entr'
-p postpones the first run until something actually changes (otherwise entr runs your command once immediately, which is usually what you want, but not while you’re demonstrating). -s evaluates the argument with your shell so you can use a pipeline. Here’s what we captured editing greet.sh twice:
[run] hello, entr
bash returned exit code 0
[run] hello, entr
bash returned exit code 0
Two edits, two runs. The bash returned exit code 0 line is entr itself, from -s: when standard output is a TTY it prints the shell name and the exit code after each invocation, so a failing test run is visible at a glance. That is the entire value proposition — a tight edit → save → see-the-result loop with none of your own while boilerplate. For the common case (ls *.py | entr -c pytest, find . -name '*.md' | entr -c make) it is genuinely a one-liner.
Surprise one: it wants a TTY, and dies in a script without -n
The first time we tried to demonstrate entr from a non-interactive shell — which is to say, from a script, a CI job, or anything without a controlling terminal — it refused to start:
$ echo greet.sh | entr -s 'echo ran'
entr: unable to get terminal attributes, use '-n' to run non-interactively
By default entr reads the keyboard (space runs the command on demand, q quits) and to do that it wants a real terminal. No terminal, no entr. The fix is right there in the error, -n / non-interactive mode, which tells it not to touch the TTY:
$ echo greet.sh | entr -n -s 'echo ran'
That now works headless. It’s an easy fix once you’ve seen it, but the first time it bites you’ll assume the pipe is broken, not that the watcher wanted a terminal. If you’re wiring entr into anything automated, -n is not optional.
Surprise two (the headline): it watches a fixed list — new files are invisible
This is the one that costs people an afternoon. entr reads the file list once, at startup, and watches exactly those files forever. A file created after entr starts is not in the list, so entr will never fire for it — even though your ls glob would match it now.
We started entr watching *.txt with one file present, then created b.txt and separately touched the already-watched a.txt:
$ ls *.txt | entr -n -p -s 'echo "[triggered] files: $(ls *.txt | tr "\n" " ")"'
(creating b.txt AFTER entr started) <- nothing happens
(appending to a.txt) <- fires:
[triggered] files: a.txt b.txt
Only the change to a.txt triggered a run. b.txt was invisible to the watcher the entire time — note that when the command did run, its own ls happily lists b.txt, which is exactly how this fools you: the file is right there, so you assume entr saw it appear. It didn’t. The list was frozen at launch.
The fix entr ships for this is -d, which also tracks the directories of your files and exits when a new file shows up:
$ ls *.txt | entr -d -n -p -s 'echo "[run]"'
(creating y.txt)
entr: directory altered
entr exited, rc=2
Exit code 2, with directory altered on stderr. That looks like a crash; it’s the intended behavior. The idiom is to wrap the whole thing in a shell loop so a fresh entr re-reads the (now larger) file list:
while true; do
find . -name '*.txt' | entr -d -n -s 'echo "[run]"'
done
Now new files rejoin the watch on the next loop. It feels like a hack because it is one — but it’s the documented pattern, and once you internalize “entr watches a snapshot, not a glob,” the loop stops feeling strange.
The bits that make it earn its keep
/_ is the file that changed. In exec mode (no -s), the token /_ expands to the path of the file that triggered the run, so you can act on just it:
$ echo note.md | entr -n -p /usr/bin/echo " changed:" /_
changed: /tmp/tmp.SC5Zr7vfHl/note.md
Handy for ... | entr -n rubocop /_ to lint only what you touched. (This works in exec mode; inside a -s shell string it’s the shell’s job, not entr’s.)
-r restarts a long-running process. Without it, entr runs your command to completion and won’t run it again until the next change — fine for a test suite, useless for a server that never exits. -r sends the child a SIGTERM and relaunches it on each change. We watched a sleep-forever “server” and edited its file once:
server up, pid 8300
(editing server.sh)
server up, pid 8303
Two different PIDs: it killed the old process and started a fresh one. ls src/**/*.js | entr -r node server.js is the entire live-reload story, no nodemon required.
-c clears the screen before each run (twice, -cc, also wipes scrollback), so your terminal shows only the latest result instead of an ever-growing scroll of test output.
Where it falls down, honestly
entr is a snapshot watcher wearing a stopwatch, and there are jobs it isn’t built for:
- It won’t discover files on its own. Everything above is the tax for the design: you either re-feed the list in a loop (
-d) or accept that new files are ignored. Tools likewatchexecandnodemonwatch a directory tree live and pick up new files automatically — if your project churns files constantly, that model fits better, and entr’s loop-around--dwill feel like fighting the tool. - Big trees cost you. On Linux entr uses inotify, which places one watch per file. Point it at a
node_modules-sized tree and you can hit the per-usermax_user_watcheslimit; the fix is to narrow the file list (which you should be doing anyway) or raise the sysctl. We didn’t reproduce the limit here — our test trees were tiny — so take this as the documented behavior, not something we captured. - It’s a launcher, not a build system. entr has no idea what your command does, no dependency graph, no caching. It reruns the whole command every time. That’s a feature (dead simple) until your command takes 90 seconds, at which point you want a real incremental build behind it and entr merely as the trigger.
One thing we half-expected to break and didn’t: editors that save atomically by writing a temp file and renaming it over yours. On some setups that swaps the inode out from under a watcher. entr 5.5 on Linux followed the rename fine — both the atomic replace and a plain in-place append fired the command. So we’re not going to warn you about a failure we couldn’t make happen; on this box it worked.
What it costs and the free alternative
It costs nothing — ISC-licensed, no account, no telemetry, no paid tier. The “free alternative” question is really “what else watches files”: watchexec (Rust, watches a tree live), nodemon (Node-flavored, restart-focused), and inotifywait in a hand-rolled loop (the from-scratch version entr saves you from). The honest trade is philosophy: entr wins on being tiny, obvious, and pipe-native — it composes with find, git ls-files, rg -l, anything that prints filenames — and it loses the moment you want a watcher that keeps up with a directory that’s growing on its own.
What made us close the tab
Nothing — entr earned a spot next to fzf and rg in the “how did I work without this” pile. The caveats, in the order they’ll bite you:
- It watches a fixed list, not a glob. Files created after startup are invisible. Use
-dinside awhileloop if your file set changes, and stop expecting it to notice new files on its own. - It needs a TTY unless you pass
-n. In any script or CI job,-nis mandatory or it exits withunable to get terminal attributes. - It reruns the whole command, every time. No caching, no dependency graph. Keep the command fast, or put a real build tool behind the trigger.
When it goes wrong: if entr “isn’t firing,” the culprit is almost always the fixed list — you added a file it never saw. Confirm by touching a file that existed when entr started; if that fires, you’ve found it, and the answer is -d plus a loop. If it won’t start at all, you’re headless and missing -n. And if it runs but your server doesn’t reload, you forgot -r. Three flags, three failures, one very small tool that does exactly what its one-line usage told you it would.