Skip to content

fix(filesystem): write in place to preserve birthtime and file identity (#4512)#4516

Open
0ywfe wants to merge 1 commit into
modelcontextprotocol:mainfrom
0ywfe:fix/4512-preserve-birthtime-on-write
Open

fix(filesystem): write in place to preserve birthtime and file identity (#4512)#4516
0ywfe wants to merge 1 commit into
modelcontextprotocol:mainfrom
0ywfe:fix/4512-preserve-birthtime-on-write

Conversation

@0ywfe

@0ywfe 0ywfe commented Jul 11, 2026

Copy link
Copy Markdown

Summary

Closes #4512write_file and edit_file destroyed the file's creation timestamp (birthtime) and broke hard links on every write because the temp-file + fs.rename pattern replaced the inode.

Root cause

Both writeFileContent and applyFileEdits used temp-file + fs.rename for existing files. While this provided crash-atomicity and symlink safety, rename replaces the target with a different file (new inode), destroying:

  • Creation timestamp (birthtime on macOS/APFS, creation time on Windows/NTFS, crtime on ext4)
  • Hard link identity (severed)
  • Inode-based file watchers/editors (lost)

The server's own get_file_info reports created: as a first-class field — silently destroying it on write is internally inconsistent.

Fix

Replace the temp+rename pattern for existing files with in-place writes via O_RDWR | O_NOFOLLOW:

  1. fs.open(filePath, O_RDWR | O_NOFOLLOW) — rejects symlinks with ELOOP, matching the security guarantee the rename pattern provided (no writes through pre-existing symlinks).
  2. fh.truncate(0) + fh.write(content) — writes through the existing file handle, preserving the inode, birthtime, and hard links.

New files still use the wx flag (exclusive create) — unchanged.

Both writeFileContent (EEXIST branch) and applyFileEdits (non-dry-run branch) now use the shared writeInPlace helper.

Trade-off

In-place writes lose crash-atomicity: a crash mid-write can leave a partially written file, which the rename pattern avoided. For the MCP filesystem server's use case (writing text files on behalf of an AI agent, not a database), birthtime/identity preservation is the more useful default. If crash-atomicity is essential for a specific deployment, a follow-up could add an opt-in flag to select the rename strategy.

Test plan

  • npm run build — TypeScript clean
  • npm test — all 153 tests pass (7 test files)
  • Updated all applyFileEdits tests to mock the new fs.open + FileHandle path instead of writeFile + rename
  • Added a dedicated test for writeFileContent's EEXIST → writeInPlace flow
  • Verified O_NOFOLLOW rejects symlinks with ELOOP on macOS (Node.js exposes fs.constants.O_NOFOLLOW)

Files

  • src/filesystem/lib.tswriteInPlace helper, updated writeFileContent + applyFileEdits, removed unused randomBytes import
  • src/filesystem/__tests__/lib.test.ts — updated mocks + new test

…ty (modelcontextprotocol#4512)

writeFileContent and applyFileEdits replaced existing files via a
temp-file + fs.rename pattern. While this provided crash-atomicity and
symlink safety, every write created a new inode, destroying the file's
creation timestamp (birthtime) and severing hard links. The server's
own get_file_info reports birthtime as a first-class field, making
this internally inconsistent.

Replace the temp+rename pattern for existing files with an in-place
write using O_RDWR | O_NOFOLLOW:
  1. fs.open(filePath, O_RDWR | O_NOFOLLOW) — rejects symlinks with
     ELOOP, matching the security guarantee of the rename pattern.
  2. fh.truncate(0) + fh.write(content) — writes through the existing
     file handle, preserving the inode, birthtime, and hard links.

New files still use the wx flag (exclusive create), which is unchanged.

Trade-off: in-place writes lose crash-atomicity (a crash mid-write can
leave a partially written file). The rename pattern avoided this. If
crash-atomicity is considered essential for a specific deployment, a
follow-up could add an opt-in flag to select the rename strategy.

Tests updated to mock the new fs.open + FileHandle path. Added a
dedicated test for the EEXIST -> writeInPlace flow in writeFileContent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant