fs.watch on Linux: an event about the watched directory itself differs from Node (event type, filename for a trailing slash, recursive root)
Author: robobunCreated Sep 17, 2026Updated Sep 17, 2026
Labelsbuglinuxnode:fs
What happens
On Linux, fs.watch() on a directory reports an event about the watched directory itself (for example a chmod of it) differently from Node v26.3.0. There are three differences:
- The event type is
change. Node reportsrename. - For a watched path with a trailing slash, or with
.as its last segment, Node reports""or"."asfilename. Bun reports the directory name. - A recursive watch reports
changewithfilename === undefined. Node reports nothing.
Found during the work on #43064. That PR does not change these.
Reproduction
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const sleep = ms => new Promise(r => setTimeout(r, ms));
const base = fs.mkdtempSync(path.join(os.tmpdir(), "dir-self-"));
const dir = path.join(base, "watched");
fs.mkdirSync(dir);
async function run(label, watched, options) {
const events = [];
const w = fs.watch(watched, options, (e, f) => events.push(`${e}:${JSON.stringify(f)}`));
await sleep(100);
fs.chmodSync(dir, fs.statSync(dir).mode ^ 0o001);
await sleep(200);
w.close();
console.log(label.padEnd(28), JSON.stringify(events));
}
await run("fs.watch(dir)", dir, {});
await run("fs.watch(dir + '/')", dir + "/", {});
await run("fs.watch(dir + '/.')", dir + "/.", {});
await run("fs.watch(dir, recursive)", dir, { recursive: true });
fs.rmSync(base, { recursive: true, force: true });Output (Linux x64)
| call | node v26.3.0 | bun 1.4.3 |
|---|---|---|
fs.watch(dir) |
rename:"watched" |
change:"watched" |
fs.watch(dir + '/') |
rename:"" |
change:"watched" |
fs.watch(dir + '/.') |
rename:"." |
change:"watched" |
fs.watch(dir, { recursive: true }) |
no event | change:undefined |
Cause
- libuv maps every inotify mask bit outside
IN_ATTRIB|IN_MODIFYtoUV_RENAME(linux.c#L2611-L2615). For a directory the kernel also setsIN_ISDIR, so the event has bothUV_CHANGEandUV_RENAME, and Node prefersrename. The inotify dispatch insrc/runtime/node/path_watcher.rsreportsrenameonly for the create, delete and move bits. - libuv reports
uv__basename_r(path), the bytes after the last/of the path as the caller gave it (linux.c#L2625).FSWatcher::initjoins the path with the cwd and normalizes it before it reachespath_watcher::watch. #41986 changes that step. - Node's recursive watcher on Linux is
lib/internal/fs/recursive_watch.js, not libuv. It does not report an attribute change of the root. The inotify dispatch passes the empty relative path of the root toemit, and JS receivesundefined.
The first difference is the one with a practical effect. The other two are listed for completeness.
Source: oven-sh/bun