Repository navigation
Description
Summary
write_file and edit_file replace existing files via a temp-file + fs.rename pattern (writeFileContent and applyFileEdits in src/filesystem/lib.ts). There is a security rationale in the code comments: atomic rename prevents symlink race conditions (TOCTOU) between path validation and write. However, this strategy has an undocumented side effect: every write to an existing file creates a new inode, which
- destroys the file's creation timestamp (
birthtimeon macOS/APFS and BSD, creation time on Windows/NTFS,crtimeon ext4), and - breaks file identity: hard links are severed, and inode-based file watchers/editors tracking the file lose it.
Notably, the server itself treats the creation timestamp as first-class metadata: get_file_info returns it as a dedicated created: field. Reporting this field but silently destroying it on each of its own write operations is internally inconsistent.
To reproduce
- Create a file, note
birthtime(e.g.stat -f "%SB" file.mdon macOS, or via the server's ownget_file_info). - Call
edit_file(orwrite_file) on it. - Read
birthtimeagain → it now equals the time of the edit.
(move_file correctly preserves birthtime, since rename keeps the inode.)
Root cause
writeFileContent falls back to temp-file + rename for existing files; applyFileEdits uses the same pattern unconditionally. rename replaces the target with a different file, so all creation metadata and identity of the original are lost.
Suggested fix
Preserve the symlink-safety property while writing in place:
fs.open(filePath, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW)— fails withELOOPif the path is a symlink, giving the same protection the rename pattern provides (no writes through symlinks swapped in after validation).fstatthe handle and verify it is a regular file (defense in depth; also allows re-checking the resolved path if desired).ftruncate+ write through the handle → same inode,birthtime, hard links, and watchers all preserved.
On POSIX platforms (macOS, Linux, BSD) O_NOFOLLOW is well supported. On Windows, symlink creation requires elevated privileges by default and the threat model differs; the current rename path could be retained there, or FILE_FLAG_OPEN_REPARSE_POINT semantics used.
Trade-off: In-place writes lose crash-atomicity (a crash mid-write can leave a partially written file, which the rename pattern avoids). If that property is considered essential, an alternative would be an opt-in mode (env var or tool argument, e.g. preserveFileIdentity) selecting the in-place strategy — or at minimum, documenting the metadata-destroying behavior of the current implementation in the tool descriptions.
Activity
ayushbunny11 commentedon Jul 11, 2026
Going with your suggestion to open with
O_RDWR|O_NOFOLLOW,fstatto confirm it's a regular file before touching it, then truncate + write.Keeping the old rename path on win32, since
O_NOFOLLOWisn't reliable there and symlinks already need admin rights to create on Windows anyway.Only real downside is losing crash-atomicity on
POSIX; a crash mid-write now leaves a truncated file instead of an untouched one.fix(filesystem): write in place to preserve birthtime and file identi…
fix(filesystem): write in place to preserve birthtime and file identi…
fix(filesystem): write in place to preserve birthtime and file identi…
fix(filesystem): write in place to preserve birthtime and file identi…
test(filesystem): characterize every tool in-process and gate coverag…
test(filesystem): characterize every tool in-process and gate coverag…
test(filesystem): characterize every tool in-process and gate coverag…
docs(filesystem): read-only files are refused by the in-place write (#…
Merge pull request #5022 from modelcontextprotocol/v2/fix/4512-filesy…
cliffhall commentedon Oct 5, 2026
Fixed by #5022, merged into
v2/main.fix(filesystem): overwrite existing files in place (modelcontextproto…