#43065·bun

fs.watch recursive on Linux: no event for a change behind a symlink entry that points outside the tree (regression in 1.3.14)

Author: robobunCreated Sep 17, 2026Updated Sep 17, 2026
Labelsbugregressionlinuxnode:fs

What happens

On Linux, a recursive fs.watch() does not report a change that happens behind a symlink entry of the watched tree, when the link target is outside the tree. Node v26.3.0 and bun 1.3.13 report it. bun 1.3.14 and later report nothing.

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

Reproduction

import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rec-symlink-"));
fs.mkdirSync(path.join(dir, "real", "target-dir"), { recursive: true });
fs.mkdirSync(path.join(dir, "watched"));
fs.writeFileSync(path.join(dir, "real", "target.txt"), "x");
fs.writeFileSync(path.join(dir, "watched", "plain.txt"), "x");
fs.symlinkSync(path.join(dir, "real", "target.txt"), path.join(dir, "watched", "link.txt"));
fs.symlinkSync(path.join(dir, "real", "target-dir"), path.join(dir, "watched", "link-dir"));

const events = [];
const w = fs.watch(path.join(dir, "watched"), { recursive: true }, (e, f) => events.push(`${e}:${f}`));
setTimeout(() => fs.appendFileSync(path.join(dir, "real", "target.txt"), "y"), 100);
setTimeout(() => fs.writeFileSync(path.join(dir, "real", "target-dir", "child.txt"), "y"), 300);
setTimeout(() => fs.appendFileSync(path.join(dir, "watched", "plain.txt"), "y"), 500);
setTimeout(() => {
  w.close();
  console.log(JSON.stringify(events));
  fs.rmSync(dir, { recursive: true, force: true });
}, 800);

Output (Linux x64)

runtime events
node v26.3.0 ["rename:link.txt","rename:link-dir","rename:link-dir","change:plain.txt"]
bun 1.3.13 ["change:link.txt","rename:link-dir/child.txt","change:link-dir/child.txt","change:plain.txt"]
bun 1.3.14, bun 1.4.3 ["change:plain.txt"]

The event for plain.txt shows that the watcher works. The events for link.txt and link-dir are missing.

Probable cause

#29952 (first release: 1.3.14) replaced the fs.watch() backend. The new inotify backend in src/runtime/node/path_watcher.rs adds one watch for each directory of the tree. walk_subtree handles an entry as a directory only when entry.kind == Directory, so it skips a symlink entry. A file event arrives on the watch of the parent directory of the file. The parent directory of the link target is outside the tree, so no watch covers it.

Node's recursive watcher on Linux (lib/internal/fs/recursive_watch.js) adds a watch for each entry, and inotify_add_watch follows the symlink.

The comment in Linux::add_one says that the walk does not follow a symlink to a directory on purpose, to avoid a cycle. A fix must keep that protection.