modelcontextprotocol / modelcontextprotocol/servers

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

Open
#4,512 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
TypeScript
Stars
90.5k
Forks
11.7k
Avg merge
2d 2h
Merged PRs (30d)
5

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 (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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/filesystem/lib.ts by reading writeFileContent and applyFileEdits, including their security comments and existing-file write paths. Run the birthtime and hard-link reproduction against write_file and edit_file; done means the chosen behavior preserves file identity and creation metadata without weakening symlink protection, with platform and crash-atomicity trade-offs addressed.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.