modelcontextprotocol / modelcontextprotocol/servers
filesystem: `write_file`/`edit_file` destroy file creation time (birthtime) and file identity due to atomic-rename write strategy
Nobody has claimed this yet.
- 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 (
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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