Skip to content

filesystem: write_file/edit_file destroy file creation time (birthtime) and file identity due to atomic-rename write strategy #4512

Closed

Description

@DannyAmmon

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

  1. Create a file, note birthtime (e.g. stat -f "%SB" file.md on macOS, or via the server's own get_file_info).
  2. Call edit_file (or write_file) on it.
  3. 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:

  1. 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).
  2. fstat the handle and verify it is a regular file (defense in depth; also allows re-checking the resolved path if desired).
  3. 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

  1. ayushbunny11 commented on Jul 11, 2026

    @ayushbunny11
    Contributor

    Going with your suggestion to open with O_RDWR | O_NOFOLLOW, fstat to confirm it's a regular file before touching it, then truncate + write.

    Keeping the old rename path on win32, since O_NOFOLLOW isn'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.

  2. added
    server-filesystemReference implementation for the Filesystem MCP server - src/filesystem
    on Oct 4, 2026
  3. added this to the v2.0.0 milestone on Oct 4, 2026
  4. self-assigned this
    on Oct 4, 2026
  5. added 2 commits that reference this issue on Oct 5, 2026
  6. cliffhall commented on Oct 5, 2026

    @cliffhall
    Member

    Fixed by #5022, merged into v2/main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingserver-filesystemReference implementation for the Filesystem MCP server - src/filesystemv2

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions