# Watcher: one C++ filesystem watcher, bound for six languages

> Watcher is a small C++ library that watches the filesystem and reports events through a single header, with bindings for C, Python, Rust, Node.js and Go, and a command line program that prints the same events as JSON. The C++ runtime is 1579 lines, which is the point the project makes about it.

**e-dant/watcher** — Filesystem watcher. Works anywhere. Simple, efficient and friendly.

- Repository: https://github.com/e-dant/watcher
- Stars: 810 · Forks: 47
- Language: C++
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/e-dant-watcher

## A C++ core with bindings around it

Watcher is a filesystem event watcher whose description is three words: simple, efficient and friendly. The implementation is C++ with a header-only style API, and the project is MIT licensed by Will.

The interesting decision is the shape of the distribution. There is one core implementation, and the repository carries a directory per language that binds to it, watcher-c, watcher-go, watcher-nodejs, watcher-py and watcher-rs, alongside the include and src directories of the core itself. So a Python developer, a Rust developer and a Go developer all get the same watcher rather than three implementations with three sets of bugs.

The packaging is correspondingly wide. The badges in the README show a Conan Center recipe, a crate on crates.io as wtr-watcher, a PyPI package as wtr-watcher, a Go module under the watcher-go path, a Homebrew formula, packages in nixpkgs stable and unstable, and a GNU Guix package. The continuous integration is similarly broad, with separate workflows for distribution builds, CodeQL, Linux tests, macOS tests, Android compilation tests and Windows tests.

That combination is the argument the project makes. If you only write C++, the core is enough. If you write something else, there is a package in your ecosystem's registry, and the behaviour on a macOS laptop and a Linux server is the same because it is the same code.

The default branch is release, the last push to the repository was on 2026-07-05, and the recent releases are 0.14.5 in February 2026, 0.14.4 in February and 0.14.3 in January.

## The event object and the 1579 lines behind it

The API is small because the runtime is small. The README points at the 1579 lines that make up the runtime of Watcher, in the single header include/wtr/watcher.hpp, and presents that as the reason the library can be simple and practical at the same time.

The API is one call. You pass a path and a function, and the function is called on each event:

```cpp
auto w = watch(path, [](event ev) { cout << ev; });
```

The event is a small struct with a handful of fields, and the design of those fields is what makes the library pleasant to use. The event type, and every field within it, has string conversions and stream operators, across narrow, wide and other string types. So an effect type, a path type and a path name can be streamed directly, and the same event can be json-serialized when you want the machine-readable form instead of your own formatting. The C++ example in the README writes its own formatter to show the point, and notes that a different effect type, path type or path name can be produced by streaming the event itself.

The two parts of an event worth understanding are the effect type, which says what happened, and the associated path, which says what it happened to. That is what lets a rename arrive as one event with a source and a destination rather than as a create followed by a delete, which is the classic annoyance of filesystem watching APIs.

The program form prints events as JSON, and the sample output in the README shows a modify, a rename with the arrow between the two paths, and a create.

## Using it as a library from six languages

The quick start section gives one example per language, and the shape is consistent enough to read any of them.

In C++ you include the header, call watch with a path and a callback, and close the watcher when you are done:

```cpp
auto main() -> int {
  // Watch the current directory asynchronously,
  // calling the provided function on each event.
  auto watcher = watch(".", show);

  // Do some work. (We'll just wait for a newline.)
  getchar();

  // The watcher would close itself around here,
  // though we can check and close it ourselves.
  return watcher.close() ? 0 : 1;
}
```

The C binding is the lowest common denominator, a callback and an opaque watcher handle:

```c
int main() {
    void* watcher = wtr_watcher_open(".", callback, NULL);
    getchar();
    return ! wtr_watcher_close(watcher);
}
```

Python is a context manager, installed from PyPI:

```bash
pip install wtr-watcher
```

```python
from watcher import Watch

with Watch(".", print):
    input()
```

Rust is an async stream, with the crate added through cargo:

```rust
use futures::StreamExt;
use wtr_watcher::Watch;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let show = |e| async move { println!("{e:?}") };
    let events = Watch::try_new(".")?;
    events.for_each(show).await;
    Ok(())
}
```

Node.js uses a callback and a close method, and Go uses a constructor with a callback and a deferred Close:

```go
func main() {
	w := watcher.NewWatcher("/path/to/dir", func(e *watcher.Event) {
		slog.Info("filesystem event", "event", e)
	})
	defer w.Close()

	// Wait for a new line to exit
	_, _ = fmt.Scanln()
}
```

The Rust example is the one that shows the cost of the approach most clearly, since the crate is built with a build script that compiles the C++ core and generates bindings, and the library itself is a thin Rust layer over the same C API.

## As a program, it prints JSON

The second half of the design is that you do not have to write any code. Watcher may be used as a library, a program, or both, and if you are not looking to build something with the library you can just use the program, which prints filesystem events as JSON.

The project name in that mode is wtr.watcher, and the README gives three ways to get it.

The first is to clone the repository, and the second and third are the build options. The build script path builds a release build for the host platform and leaves the binary in a directory you cd into:

```bash
# The main branch is the (latest) release branch.
git clone https://github.com/e-dant/watcher.git && cd watcher
# Via Nix
nix run | grep -oE 'cmake-is-tough'
# With the build script
tool/build --no-build-test --no-run && cd out/this/Release # Build the release version for the host platform.
./wtr.watcher | grep -oE 'needle-in-a-haystack/.+"' # Use it, pipe it, whatever. (This is an .exe on Windows.)
```

The nix run and the build script are the two practical routes, and the comment about the binary being an exe on Windows is the kind of detail that matters when a script has to work everywhere.

There is a standalone compile path too, for someone who just wants the C++ core without the build system. It needs C++17, and on macOS it needs the CoreFoundation and CoreServices frameworks, which the example spells out as a platform specific variable:

```sh
# Sigh
PLATFORM_EXTRAS=$(test "$(uname)" = Darwin && echo '-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -framework CoreFoundation -framework CoreServices')
# Build
eval c++ -std=c++17 -Iinclude src/wtr/tiny_watcher/main.cpp -o watcher $PLATFORM_EXTRAS
# Run
./watcher
```

So a project that already compiles C++ can vendor a single header and one source file and be watching files in the afternoon.

## Claiming you can watch an entire filesystem

The efficiency claim is the boldest part of the project's description, and it comes with a link to a section about the exceptions to efficient scanning.

The claim is that you can watch an entire filesystem with this project, that in almost all cases the tool uses a near-zero amount of resources, and that it makes efficient use of the cache. The README also says the project regularly tests the overhead of detecting and sending an event to the user.

This is the part of the design where the platform matters most. Watching a directory tree means asking the operating system what changed, and the mechanism differs: on Linux there are inotify watches, on macOS there is a kernel event mechanism, and on Windows there is a different one again. A library that claims to work everywhere has to hide those differences behind one interface, and the cost of hiding them is a scanning strategy that is efficient for most directories and inefficient for a few.

That is why the exceptions section exists, and it is why the claim is phrased as almost all cases rather than as a guarantee. The cache efficiency link points at a second section describing how the tool avoids re-reading directory contents that have not changed, which is the usual way a naive recursive watcher burns IO on a large tree.

For a build system that watches a source tree, or a test harness watching fixtures, that is more than sufficient. For a root filesystem with a million files and an unusual layout, the honest reading of the documentation is that you should check the exceptions section first.

## The bindings are generated, and the packaging is broad

The build surface of the repository is wide, and the reason is that it tries to meet every ecosystem on its own terms.

The core is a C++ library, and the Rust crate compiles it with a build script using cc and bindgen, which is the standard approach for generating bindings to a C API. The Rust package then wraps that in an async stream and gates its command line feature behind a cli feature, with cli and serde enabled by default.

The Python package takes a different route, building through meson-python with meson as the build backend. Its configuration is where the platform coverage is spelled out: cibuildwheel builds for manylinux on x86_64 and aarch64, musllinux on x86_64 and aarch64, PyPy variants, and a free-threaded build, while older Python versions are skipped to save build time, and macOS is built for arm64 only because of a bug in the build tool.

Around those, the repository carries CMakeLists.txt, meson.build, BUILD.bazel and WORKSPACE, a Cargo.toml, a go.mod, a pyproject.toml, a flake.nix, a .version file, a watcher.pc.in template for pkg-config, a changelog, a contributing guide and a security policy.

That breadth is a strength and a cost. The strength is that a project can consume Watcher without changing its build system, whether that is CMake, Bazel, Meson, Cargo, pip, Go modules, Nix, Conan, Homebrew or Guix. The cost is that a bug in any of those layers is a bug in this project, and that a contributor who wants to add a binding has to work inside a build system that already supports nine.

## When to reach for something else

The honest case against Watcher is that it is a solution to a cross-platform problem, and if you do not have that problem it is overhead.

A Python project that only ever runs on Linux, in a build system, could use a Python-native watcher and never touch a C++ core, a bindgen build step or a meson-python wheel. A Rust project on Linux could use the tokio filesystem interfaces directly. In both cases, the thing Watcher provides is exactly the thing they do not need, which is a uniform behaviour across operating systems, and the cost of that uniformity is a binding layer, a generated C API and a set of build files for ecosystems you may never use.

The language coverage is also the practical limit. The README shows C, C++, Python, Rust, Node.js and Go. A team working in C#, Java, Kotlin or Ruby has no binding in the list, and would have to go through the C API directly, which is possible but is no longer the friendly path the project is selling.

The other case for using it is the program form. If the real requirement is to pipe filesystem events into a script, or into another language that has no watcher at all, then wtr.watcher printing JSON on standard output is a genuinely good answer, and it is the same answer on every platform.

For anyone else, the decision is whether cross-platform uniformity is worth a dependency on a C++ core. In a team that deploys to Linux, Windows and macOS from one codebase, it usually is. In a team that does not, it is a well-built library solving a problem you do not have.

## Conclusion

watcher suits a project that wants one filesystem watching behaviour across platforms and several languages, and that would rather print JSON events than embed a platform specific library, and it is the wrong tool for a single language project with a good native watcher already available, since the binding layer and the build systems come with it. Verify first that a binding exists for your language, because the README shows C, C++, Python, Rust, Node.js and Go and anything else would go through the C API, and read the exceptions to efficient scanning section, since that is where the near-zero resource claim stops being true.

## FAQ

### How do I use Watcher in Python?

Install it with pip install wtr-watcher, then use the Watch context manager from the watcher module, for example with Watch(".", print): and then input() to keep the process alive while events are printed.

### Does Watcher have bindings for Rust, Go and Node.js?

Yes. The crate is wtr-watcher added with cargo add wtr-watcher tokio futures, the Go module is github.com/e-dant/watcher/watcher-go with watcher.NewWatcher, and the Node.js package exposes a watch function. There is also a C API through the watcher-c.h header.

### Can I use Watcher without writing any code?

Yes, it ships as a program that prints filesystem events as JSON. You can run it through nix run, or build it with the build script and run the resulting wtr.watcher binary, which is an exe on Windows.

### Where can I get Watcher from?

There is a Conan Center recipe, a crate on crates.io as wtr-watcher, a PyPI package as wtr-watcher, a Go module, a Homebrew formula, packages in nixpkgs stable and unstable, and a GNU Guix package. The repository itself is cloned from github.com/e-dant/watcher.

### What licence is Watcher released under?

MIT, with the licence text in the license file at the root of the repository. The Python project metadata points at the same file, and the Rust package declares the same licence.

## Sources

- [e-dant/watcher on GitHub](https://github.com/e-dant/watcher)
- [Issues](https://github.com/e-dant/watcher/issues)
- [License: MIT](https://github.com/e-dant/watcher/blob/release/LICENSE)
- [README](https://github.com/e-dant/watcher/blob/release/README.md)
- [Releases](https://github.com/e-dant/watcher/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/e-dant-watcher
