A lightweight, zero-dependency Linux file watcher daemon built on inotify. Watches files and directories for filesystem events and optionally executes commands when specific events occur.
Requires Go 1.21 or later.
git clone <repo-url> && cd File-Watcher
go build -o fwt .This produces a single binary fwt.
Watch a directory and print events to the terminal:
./fwt start /path/to/watchIn a separate terminal, query the daemon:
./fwt summary # state + recent events
./fwt entries # list watched paths
./fwt stop # shut down the daemonLaunch the watcher daemon.
fwt start [flags] <path> [<path> ...]
| Flag | Default | Description |
|---|---|---|
| --path-file | Path to a JSON pathfile (see below) | |
| --log-level | info | Log level: debug, info, warn, error |
| --log-file | Write JSON logs to this file | |
| --no-terminal | false | Suppress terminal output |
Examples:
# Watch two directories
./fwt start /home/user/src /home/user/docs
# Watch from a pathfile with triggers
./fwt start --path-file watches.json
# Run as a quiet background logger
./fwt start --path-file watches.json \
--log-file /var/log/fwt.log \
--log-level warn \
--no-terminal &Print the current daemon state: active watches, open files, and recent events.
./fwt summaryList all currently watched paths with their trigger rules.
./fwt entriesAdd or remove watches on a running daemon. Accepts the same pathfile schema as
start, plus individual --add / --remove flags.
fwt update [flags]
| Flag | Description |
|---|---|
| --path-file | JSON pathfile with add/remove entries |
| --add | Single path to add |
| --remove | Single path to remove |
| --trigger | Trigger action for the added path (e.g. IN_MODIFY) |
| --cmd | Command to run when the trigger fires |
Examples:
# Add a path with a trigger
./fwt update --add /tmp/inbox \
--trigger IN_CREATE \
--cmd "echo new file"
# Remove a path
./fwt update --remove /tmp/inbox
# Bulk update from a pathfile
./fwt update --path-file changes.jsonGracefully shut down the running daemon.
./fwt stopBoth start and update accept the same JSON schema. Each entry has an
optional command field that is either "add" (default when omitted) or
"remove".
{
"watches": [
{
"path": "/home/user/src",
"command": "add",
"trigger_action": "IN_MODIFY",
"execute_cmd": "make build"
},
{
"path": "/home/user/logs",
"command": "add"
},
{
"path": "/tmp/old-watch",
"command": "remove"
}
]
}| Field | Required | Description |
|---|---|---|
| path | yes | Absolute or relative path to watch |
| command | no | "add" (default) or "remove" |
| trigger_action | no | Inotify event name (e.g. IN_MODIFY) |
| execute_cmd | no | Shell command to run on trigger |
When used with fwt start, entries with command: "remove" are ignored
(there is nothing to remove at startup). When used with fwt update, both
add and remove entries are processed.
Any standard inotify event name, with or without the IN_ prefix:
ACCESS, MODIFY, ATTRIB, CLOSE_WRITE, CLOSE_NOWRITE,
OPEN, MOVED_FROM, MOVED_TO, CREATE, DELETE,
DELETE_SELF, MOVE_SELF, ALL_EVENTS
go test ./... -vThe test suite includes unit tests for the protocol layer and state management, plus integration tests that spin up a full server, exercise all client commands, create/delete/rename files, and verify event tracking.
fwt is a single binary that acts as both server and client, communicating
over a Unix domain socket (/tmp/fwt.sock).
fwt start fwt summary / entries / update / stop
| |
v v
[Server] <--- UDS ---> [Client]
|
+-- Watcher (inotify event loop)
+-- Listener (UDS request handler)
+-- State (shared, mutex-protected)
+-- Logger (file + terminal)
- Watcher: Wraps a non-blocking inotify fd with
os.NewFilefor Go runtime poller integration. Recursively watches subdirectories. Detects renames by correlating MOVED_FROM / MOVED_TO cookies. - Listener: Accepts JSON requests over UDS and dispatches to State and Watcher.
- State: All mutable data (watches, events, open files, pending renames)
protected by a single
sync.RWMutex. - Logger: Leveled, thread-safe logging to both terminal (colored) and file (JSON lines).
Recursive execution is possible. If a triggered command writes to a path that is itself being watched (directly or via a parent directory), the write will generate a new event, which will trigger the command again, creating an infinite loop. There is currently no built-in debouncing or loop detection.
To avoid this:
- Make sure triggered commands do not write to watched directories.
- Redirect command output to a path outside the watched tree (e.g. to
/dev/nullor a dedicated log directory that is not watched). - If using
--log-file, ensure the log file is not inside a watched directory.