Watchexec: A Universal File Watcher That Runs Any Command
Every language ecosystem ships its own file-watcher: nodemon for Node, cargo watch for Rust, pytest-watch for Python, air for Go. They all do the same job — watch files, re-run a command — but each has a different config format, debouncing quirk, and list of gotchas.
Watchexec is the language-agnostic version. It's a single Rust binary that watches a directory (or specific files) and runs any command you hand it. No config file, no plugin ecosystem, no framework assumptions.
Installation
Watchexec ships as a static binary:
# macOS
brew install watchexec
# Cargo (any platform)
cargo install watchexec-cli
# Arch
pacman -S watchexec
# Nix
nix-env -iA nixpkgs.watchexec
Verify it works:
watchexec --version
The basic pattern
Run a command every time anything under the current directory changes:
watchexec "npm test"
That's the whole mental model. By default, watchexec:
- Watches the current directory recursively
- Ignores files matched by
.gitignore,.ignore, and VCS directories - Debounces events for 100ms so a single save doesn't trigger the command twice
- Kills the running command (SIGTERM, then SIGKILL after 10s) when files change again
Filtering what triggers reruns
Watching everything is noisy. Filter by extension:
watchexec --exts ts,tsx "npm run build"
Or by glob pattern — include and exclude:
watchexec \
--filter 'src/**/*.rs' \
--ignore 'target/**' \
"cargo test"
The --filter flag is additive and uses gitignore-style globs. --ignore excludes paths; this is where you put build outputs, caches, and generated files that would otherwise cause infinite loops.
Want more cli guides? Get guides like this in your inbox — DevTools Guide delivers one free deep-dive every week.
Debouncing and clearing
For fast-saving editors (especially Neovim's atomic write, which creates backup files), bump the debounce window:
watchexec --debounce 500ms "npm test"
Clear the terminal before each run so you see only the latest output:
watchexec --clear "npm test"
Combine them for a clean test-watch experience:
watchexec --clear --debounce 300ms --exts ts "npm test"
Signal handling
The killer feature — pun intended — is how watchexec terminates the previous command. Long-running processes (dev servers, live-reload tools) need to be stopped cleanly before restarting.
By default watchexec sends SIGTERM, waits 10 seconds, then sends SIGKILL. You can override both:
watchexec --signal SIGINT --stop-timeout 2s "node server.js"
For processes that fork children (like npm run dev spawning a webpack process), use --wrap-process=session to kill the whole process group:
watchexec --wrap-process=session "npm run dev"
Without this flag, the parent npm dies but the child webpack process lingers, leaving ports held and logs confused.
Environment variables watchexec sets
Watchexec exposes what changed via environment variables, which lets you be smart in the command you run:
$WATCHEXEC_EVENTS— space-separated list ofkind:pathentries$WATCHEXEC_COMMON_PATH— common ancestor of all changed paths
For example, only re-run the test for the file that just changed:
watchexec --exts rs '
if echo "$WATCHEXEC_EVENTS" | grep -q "src/parser"; then
cargo test parser::
else
cargo test
fi
'
Integration with build tools
Watchexec replaces most tool-specific watchers. A few practical setups:
TypeScript type-checking on save:
watchexec --exts ts,tsx "tsc --noEmit"
Rebuild and restart a Go server:
watchexec --exts go --restart --signal SIGKILL "go run ./cmd/server"
Run a database migration when schema changes:
watchexec --filter 'migrations/**/*.sql' "sqlx migrate run"
Regenerate docs when markdown changes:
watchexec --exts md "mdbook build"
Watchexec vs alternatives
vs entr — entr reads file paths from stdin (find . | entr cmd) and is smaller (~50KB). Watchexec is easier for beginners because it has built-in gitignore support and doesn't require piping. Entr wins on minimal-dependency systems.
vs nodemon — nodemon is Node-specific and has a richer config file (nodemon.json). For pure Node projects, nodemon's ecosystem (plugins, hooks) may be worth it. For polyglot repos where you want one tool everywhere, watchexec is simpler.
vs cargo watch — cargo watch is just watchexec specialized for Rust, with preset filters for Cargo projects. If you already use watchexec, you don't need cargo watch separately — watchexec --exts rs "cargo test" works fine.
vs IDE-integrated watchers — IDE watchers (VS Code tasks, JetBrains file watchers) are fine when you only work in one IDE. Watchexec runs the same way across terminals, CI preview scripts, and remote sessions.
A config file, if you want one
If you find yourself typing the same flags repeatedly, watchexec 2.x reads .watchexecrc or a [package.metadata.watchexec] section in Cargo.toml. Usually a shell alias or just recipe is simpler:
# justfile
test-watch:
watchexec --clear --exts ts "bun test"
dev:
watchexec --wrap-process=session --exts ts,tsx "bun run dev"
Then just test-watch is the daily command.
Common pitfalls
- Infinite loops from build outputs — always add
--ignorefordist/,build/,target/,node_modules/. Watchexec reads.gitignoreby default, which catches most of these, but docker-mounted files or generated artifacts not in git will re-trigger forever. - Editor backup files — Neovim's swap files and VS Code's
.tmpatomic writes can trigger spurious runs. Bump--debounceto 300–500ms and use--ignore '*.swp'. - Commands that don't exit — if your command is itself a long-running process (dev server, REPL), pair
--restartwith--signalso watchexec knows it's expected to kill and relaunch rather than wait for exit.
When you don't need watchexec
If your test runner has a native --watch mode (Vitest, Jest, pytest-watch), that mode almost always beats a generic file-watcher because it can re-run only affected tests using module-graph info. Use watchexec when you need to chain tools, re-run non-test commands, or work in a language without a good native watcher.
Wrapping up
Watchexec is a focused tool: watch files, run a command, handle signals correctly. Its value is that it works the same across every language and project, so you only learn one set of flags instead of re-learning the conventions of every framework's custom watcher. For polyglot repos, monorepos, or ad-hoc "rebuild on change" scripts, it's usually the right default.
The project is active on GitHub, has a small surface area, and the documentation at watchexec.github.io is terse and accurate. If you install only one Rust CLI this year, make it this one.