Today I discovered watchexec, a modern cross-platform file watcher that has completely replaced entr in my development workflow. It executes commands when files change with much better defaults and a more intuitive interface than traditional file watching tools.

🤖 AI-Generated Content
This post was written with assistance from Claude (Anthropic’s AI assistant) based on my experiences and requirements. While the technical content and examples reflect real usage patterns, the writing was collaboratively generated. The discovery of watchexec’s advantages over entr - particularly the reduction from verbose find . -name "*.go" | entr -r go test to clean watchexec -e go 'go test ./...' - represents genuine workflow improvements in my development process.

The Core Discovery

Watchexec is a Rust-based tool that watches file system changes and triggers commands. Unlike entr, which requires explicit file listing and complex find commands, watchexec works intelligently out of the box.

What is watchexec?

Watchexec is a simple, standalone tool that watches a path and runs a command whenever files change. It’s written in Rust, cross-platform, and has sensible defaults that make it easier to use than alternatives like entr or inotifywait.

Install it with:

1
2
3
4
5
6
7
8
# Cargo
cargo install watchexec-cli

# Homebrew (macOS/Linux)
brew install watchexec

# Arch Linux
pacman -S watchexec

Hugo Development

For this blog, I use watchexec to auto-rebuild the site during development:

1
2
3
4
5
6
7
8
# Basic Hugo development server
watchexec -e md,yml,yaml,html,css,js 'hugo serve --buildDrafts'

# Only watch content and config changes
watchexec -w content -w hugo.yml -w layouts 'hugo'

# Build and serve on any change
watchexec -r 'hugo && hugo serve --port 1314'

The -e flag specifies file extensions to watch, and -w specifies directories. The -r flag restarts the command (killing the previous process) rather than running concurrent instances.

Rust Development with Cargo

For Rust projects, watchexec shines with cargo commands:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Auto-compile on save
watchexec -e rs 'cargo check'

# Run tests continuously
watchexec -e rs 'cargo test'

# Build and run
watchexec -e rs 'cargo run'

# Format and check
watchexec -e rs 'cargo fmt && cargo clippy'

# Watch specific files only
watchexec -w src -w Cargo.toml 'cargo build'

I particularly love using it during TDD workflows where I want tests to run immediately after changing code.

Go Development

Go development becomes much smoother with watchexec:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Run tests on any Go file change
watchexec -e go 'go test ./...'

# Build and run
watchexec -e go 'go run main.go'

# Build with race detection
watchexec -e go 'go build -race .'

# Run specific test package
watchexec -w internal/auth 'go test ./internal/auth'

# Format and vet
watchexec -e go 'go fmt ./... && go vet ./...'

The ability to watch specific directories with -w is especially useful in larger Go projects where you only want to run tests for the package you’re working on.

Why I Switched from entr

I used entr for years, but watchexec has several advantages:

Better Defaults

  • entr: Requires explicit file listing via find or similar
  • watchexec: Recursively watches current directory by default
1
2
3
4
5
# entr - verbose and error-prone
find . -name "*.go" | entr -r go test ./...

# watchexec - simple and intuitive  
watchexec -e go 'go test ./...'

Ignore Patterns

  • entr: No built-in ignore patterns
  • watchexec: Respects .gitignore, .ignore, and has smart defaults

Watchexec automatically ignores common build artifacts, .git directories, and other noise. With entr, I had to carefully craft find commands to avoid triggering on build outputs.

Process Management

  • entr: Basic process handling
  • watchexec: Better process lifecycle management with -r flag

The -r flag in watchexec properly kills and restarts long-running processes, which is perfect for development servers.

Cross-Platform

  • entr: Unix-only
  • watchexec: Works on Windows, macOS, and Linux

Caveats and Gotchas

While watchexec is excellent, there are some things to watch out for:

⚠️ Build Output Loops

Be careful not to watch directories that contain build outputs. If your command generates files in a watched directory, you’ll create an infinite loop.

1
2
3
4
5
# BAD - watching target/ where cargo outputs builds
watchexec -w . 'cargo build'

# GOOD - exclude target directory  
watchexec -i target/ -e rs 'cargo build'
📝 Performance with Large Projects
In very large codebases, watching too many files can impact performance. Use -w to watch specific directories or -i to ignore large directories like node_modules/ or target/.
đź’ˇ Command Escaping

Complex commands with pipes or redirections need proper shell escaping:

1
2
3
4
5
# Use quotes for complex commands
watchexec 'cargo test 2>&1 | grep -E "(test|error)"'

# Or use shell flag
watchexec -s 'cargo test | tee test.log'

Other Considerations

  • Debouncing: Watchexec debounces file changes by default (50ms), which prevents multiple rapid triggers
  • Signal Handling: Long-running processes are sent SIGTERM, then SIGKILL if they don’t exit gracefully
  • Environment: Commands run in the same directory where watchexec was started

Cheatsheet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# Basic usage
watchexec <command>                    # Watch current dir, run command on any change
watchexec -e rs,toml 'cargo check'     # Watch only .rs and .toml files
watchexec -w src 'make test'           # Watch only src/ directory
watchexec -i target/ 'cargo build'     # Ignore target/ directory

# Process control
watchexec -r 'hugo serve'              # Restart command (kill previous)
watchexec -s 'make && ./app'           # Use shell for complex commands
watchexec --delay 1s 'slow-command'    # Add delay before running

# Filtering
watchexec -e go,mod 'go test'          # Extensions (comma-separated)
watchexec -w app -w lib 'make'         # Multiple watch dirs
watchexec -i '*.tmp' -i build/ 'make'  # Ignore patterns

# Development workflows
# Hugo development
watchexec -e md,yml,html 'hugo serve --buildDrafts'

# Rust development  
watchexec -e rs 'cargo check'          # Fast syntax checking
watchexec -e rs 'cargo test'           # Run tests
watchexec -w src 'cargo run'           # Build and run

# Go development
watchexec -e go 'go test ./...'        # Run all tests
watchexec -e go 'go run main.go'       # Build and run
watchexec -w internal/api 'go test ./internal/api'  # Test specific package

# Multi-command workflows
watchexec -e rs 'cargo fmt && cargo clippy && cargo test'
watchexec -e go 'go fmt ./... && go vet ./... && go test ./...'

# Useful flags
-r, --restart           # Restart previous command
-s, --shell             # Use shell for command execution  
-w, --watch <path>      # Watch specific path
-i, --ignore <pattern>  # Ignore pattern
-e, --exts <extensions> # File extensions to watch
--delay <duration>      # Delay before running command
-v, --verbose           # Verbose output

Watchexec has become an essential part of my development workflow. Its intuitive interface and smart defaults make file watching trivial, letting me focus on code instead of build tools. If you’re still using entr or manually rerunning commands, give watchexec a try.