You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fs: rmSync() deletes a symlinked directory's contents then throws ENOTDIR when the path has a trailing slash #67186
rmSync() deletes everything inside the target directory, then throws ENOTDIR. The symlink and the directory itself survive.
rm() and fsPromises.rm() report success and delete nothing.
On v22.20.0, rmSync() throws ENOTDIR and deletes nothing, so the sync side regressed when rmSync moved to C++ in #53617 (7168295).
Additional information
Why the three behaviors differ:
rmSync() is std::filesystem::remove_all(). The trailing slash makes lstat() follow the link, so the path reports as a directory. remove_all opens the directory, unlinks each entry, then calls unlink("link/"), which fails with ENOTDIR. By then the entries are gone.
rm() and fsPromises.rm() use internal/fs/rimraf. lstat() also reports a directory, so _rmdir() calls rmdir("link/"), which fails with ENOTDIR. _rmdir() turns ENOTDIR into callback(originalErr), and originalErr is null on this path, so the call succeeds without touching anything.
POSIX rm -rf link/ deletes the directory contents, leaves the link and the directory, and exits 0. So the sync behavior matches the shell except for the error at the end.
#66673 normalizes dot segments but keeps a trailing slash, so it does not change this case.
I am happy to send a PR once there is agreement on which behavior is wanted: match rm -rf (delete contents, succeed), or treat the path as the symlink and refuse with ENOTDIR before deleting anything.
I tried this on macOS arm64 and got a different result. All three calls succeed, delete dir and its contents, and leave the symlink in place:
main (v28.0.0-pre): rmSync, rm and fs.promises.rm → ok | link: true, dir: false, dir/keep.txt: false
v22.22.2: same
This seems to come from the OS: on macOS rmdir("link/") succeeds and removes the target directory (and unlink("link/") gives EPERM), while on Linux both fail with ENOTDIR. It also matches macOS rm -r link/, which removes the directory and keeps the link.
(I checked the link with lstat, since existsSync() follows it and reports false once dir is gone.)
That is a useful data point. So the trailing slash is handled by the OS, and the OS answers differ: on Linux rmdir("link/") and unlink("link/") both fail with ENOTDIR, on macOS rmdir("link/") follows the link and removes the target.
That leaves four outcomes for the same call:
macOS, all three APIs: target directory removed, link kept (matches rm -r link/ there)
Linux rmSync() on main: contents removed, then ENOTDIR
Linux rm() / fsPromises.rm(): nothing removed, reports success
Linux rmSync() on v22: nothing removed, throws ENOTDIR
Good point about existsSync() following the link. On Linux dir survives, so the output in the report is right, but I should have used lstatSync() for the link column. I will fix the repro if a maintainer wants it updated.
The question for maintainers stays the same: should rm with a trailing slash on a symlink resolve the link (macOS behavior) on every platform, or refuse before touching anything? Either answer is fine, but today the Linux sync path does the worst of both.
Activity
vedchaudhari commentedon Nov 2, 2026
I tried this on macOS arm64 and got a different result. All three calls succeed, delete
dirand its contents, and leave the symlink in place:This seems to come from the OS: on macOS
rmdir("link/")succeeds and removes the target directory (andunlink("link/")gives EPERM), while on Linux both fail with ENOTDIR. It also matches macOSrm -r link/, which removes the directory and keeps the link.(I checked the link with
lstat, sinceexistsSync()follows it and reports false oncediris gone.)yohannesjoya commentedon Nov 2, 2026
That is a useful data point. So the trailing slash is handled by the OS, and the OS answers differ: on Linux
rmdir("link/")andunlink("link/")both fail withENOTDIR, on macOSrmdir("link/")follows the link and removes the target.That leaves four outcomes for the same call:
rm -r link/there)rmSync()on main: contents removed, thenENOTDIRrm()/fsPromises.rm(): nothing removed, reports successrmSync()on v22: nothing removed, throwsENOTDIRGood point about
existsSync()following the link. On Linuxdirsurvives, so the output in the report is right, but I should have usedlstatSync()for the link column. I will fix the repro if a maintainer wants it updated.The question for maintainers stays the same: should
rmwith a trailing slash on a symlink resolve the link (macOS behavior) on every platform, or refuse before touching anything? Either answer is fine, but today the Linux sync path does the worst of both.