#43066·bun

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:

  1. The event type is change. Node reports rename.
  2. For a watched path with a trailing slash, or with . as its last segment, Node reports "" or "." as filename. Bun reports the directory name.
  3. A recursive watch reports change with filename === undefined. Node reports nothing.

Found during the work on #43064. That PR does not change these.

Reproduction

javascript
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

  1. libuv maps every inotify mask bit outside IN_ATTRIB|IN_MODIFY to UV_RENAME (linux.c#L2611-L2615). For a directory the kernel also sets IN_ISDIR, so the event has both UV_CHANGE and UV_RENAME, and Node prefers rename. The inotify dispatch in src/runtime/node/path_watcher.rs reports rename only for the create, delete and move bits.
  2. libuv reports uv__basename_r(path), the bytes after the last / of the path as the caller gave it (linux.c#L2625). FSWatcher::init joins the path with the cwd and normalizes it before it reaches path_watcher::watch. #41986 changes that step.
  3. 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 to emit, and JS receives undefined.

The first difference is the one with a practical effect. The other two are listed for completeness.