Skip to content

bug(memory-vault): sameFileIdentity compares stat dev, which is always 0 on Windows — every ecc memory write and --body-file read fails #2626

Description

@Gelnorth

Summary

ecc memory is completely non-functional on Windows. Every write (ecc memory save, ecc memory handoff) and every --body-file read fails unconditionally.

scripts/lib/memory-vault.jssameFileIdentity() compares the dev field of a path-based stat against a handle-based fstat. On Windows, path-based stat()/lstat() always report dev = 0 while fstat() reports the real volume serial number, so the comparison can never succeed and the TOCTOU guard rejects every operation.

Reproduction

$ ecc memory init --scope user
Initialized ECC memory scopes: user

$ printf 'hello\n' | ecc memory save --title probe --scope user --kind note --stdin
Error: Memory destination changed while it was being created.

$ ecc memory save --title probe --scope user --kind note --body-file ./body.md
Error: --body-file must remain a regular, non-symlink file while it is opened.

Both messages originate from the same helper.

Root cause

// scripts/lib/memory-vault.js:124
function sameFileIdentity(left, right) {
  return left.dev === right.dev && left.ino === right.ino;
}

Call sites:

  • readRegularTextFile() line 148 — compares lstatSync(path) against fstatSync(fd); breaks --body-file.
  • the write path, line 199 — same comparison on the temp destination; breaks every save / handoff.

Measured on Node v22.15.0 / Windows 11 (10.0.26200), on two different volumes:

node v22.15.0 / win32 10.0.26200

C:\Users\<user>\AppData\Local\Temp\probe.txt
  fstat dev=1644385068  ino=21110623254304612
  lstat dev=0           ino=21110623254304612
  stat  dev=0           ino=21110623254304612
  fstat===lstat identity: false

D:\probe.txt
  fstat dev=3054669153  ino=562949953451607
  lstat dev=0           ino=562949953451607
  stat  dev=0           ino=562949953451607
  fstat===lstat identity: false

ino matches in both cases. Only dev diverges, and it diverges deterministically: handle-based fstat returns the volume serial, path-based stat/lstat return 0. The two volumes return different fstat values, confirming the value is a real volume serial rather than a constant.

Confirming the causality

Swapping only this function, with the same command and environment:

sameFileIdentity Result
current main Error: Memory destination changed while it was being created.
ino-strict, dev compared only when both non-zero Saved unreviewed note: ...

Suggested fix

Keep ino strict, and only compare dev when both sides actually report one. POSIX is unaffected because both values are non-zero there, so the original strict behaviour is preserved.

function sameFileIdentity(left, right) {
  // The inode/file-id must always match; that is the primary identity signal.
  if (left.ino !== right.ino) return false;
  // Windows reports dev=0 from path-based stat()/lstat() while fstat() on an
  // open handle reports the real volume serial, so a strict dev comparison
  // never matches and every vault read/write is rejected. Compare dev only
  // when both sides report one; on POSIX both are non-zero, so the original
  // strict check is preserved.
  if (!left.dev || !right.dev) return true;
  return left.dev === right.dev;
}

This narrows the guard slightly on Windows: a same-ino file on a different volume can no longer be distinguished. The surrounding O_NOFOLLOW, isSymbolicLink(), isFile() and assertWithinTrustedRoot() checks all still apply, so the residual exposure is small compared to the vault being unusable.

An alternative that keeps the guard fully strict would be to avoid the path-based stat entirely and compare handle-to-handle instead.

Note on ino precision

Independent of this bug: fs.Stats.ino is a JS Number, so Windows 64-bit file IDs above 2^53 silently lose precision and could make two distinct files compare equal. fs.statSync(path, { bigint: true }) would avoid that. Mentioning it because this function's correctness depends on ino.

Environment

  • ecc-universal 2.1.0 (latest on npm, published 2026-07-27)
  • Node v22.15.0
  • Windows 11 Pro 10.0.26200
  • Installed via npm install -g ecc-universal; ECC itself installed as a Claude Code plugin (ecc@ecc 2.1.0)

Impact

The Memory Vault is one of ECC's headline features and the unified-memory skill directs users to npm install -g ecc-universal to enable it. On Windows that path produces a vault that initializes cleanly, passes ecc memory doctor, and then refuses every write. The failure mode is confusing because init and doctor both succeed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions