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 (
birthtime on macOS/APFS and BSD, creation time on Windows/NTFS, crtime on 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.md on macOS, or via the server's own get_file_info).
- Call
edit_file (or write_file) on it.
- Read
birthtime again → 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 with ELOOP if the path is a symlink, giving the same protection the rename pattern provides (no writes through symlinks swapped in after validation).
fstat the 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.
Summary
write_fileandedit_filereplace existing files via a temp-file +fs.renamepattern (writeFileContentandapplyFileEditsinsrc/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, whichbirthtimeon macOS/APFS and BSD, creation time on Windows/NTFS,crtimeon ext4), andNotably, the server itself treats the creation timestamp as first-class metadata:
get_file_inforeturns it as a dedicatedcreated:field. Reporting this field but silently destroying it on each of its own write operations is internally inconsistent.To reproduce
birthtime(e.g.stat -f "%SB" file.mdon macOS, or via the server's ownget_file_info).edit_file(orwrite_file) on it.birthtimeagain → it now equals the time of the edit.(
move_filecorrectly preservesbirthtime, since rename keeps the inode.)Root cause
writeFileContentfalls back to temp-file +renamefor existing files;applyFileEditsuses the same pattern unconditionally.renamereplaces 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_NOFOLLOWis well supported. On Windows, symlink creation requires elevated privileges by default and the threat model differs; the current rename path could be retained there, orFILE_FLAG_OPEN_REPARSE_POINTsemantics 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.