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.js → sameFileIdentity() 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.
Summary
ecc memoryis completely non-functional on Windows. Every write (ecc memory save,ecc memory handoff) and every--body-fileread fails unconditionally.scripts/lib/memory-vault.js→sameFileIdentity()compares thedevfield of a path-based stat against a handle-basedfstat. On Windows, path-basedstat()/lstat()always reportdev = 0whilefstat()reports the real volume serial number, so the comparison can never succeed and the TOCTOU guard rejects every operation.Reproduction
Both messages originate from the same helper.
Root cause
Call sites:
readRegularTextFile()line 148 — compareslstatSync(path)againstfstatSync(fd); breaks--body-file.save/handoff.Measured on Node v22.15.0 / Windows 11 (10.0.26200), on two different volumes:
inomatches in both cases. Onlydevdiverges, and it diverges deterministically: handle-basedfstatreturns the volume serial, path-basedstat/lstatreturn0. The two volumes return differentfstatvalues, 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:
sameFileIdentitymainError: Memory destination changed while it was being created.ino-strict,devcompared only when both non-zeroSaved unreviewed note: ...Suggested fix
Keep
inostrict, and only comparedevwhen both sides actually report one. POSIX is unaffected because both values are non-zero there, so the original strict behaviour is preserved.This narrows the guard slightly on Windows: a same-
inofile on a different volume can no longer be distinguished. The surroundingO_NOFOLLOW,isSymbolicLink(),isFile()andassertWithinTrustedRoot()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
inoprecisionIndependent of this bug:
fs.Stats.inois a JSNumber, so Windows 64-bit file IDs above2^53silently 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 onino.Environment
ecc-universal2.1.0 (latest on npm, published 2026-07-27)npm install -g ecc-universal; ECC itself installed as a Claude Code plugin (ecc@ecc2.1.0)Impact
The Memory Vault is one of ECC's headline features and the
unified-memoryskill directs users tonpm install -g ecc-universalto enable it. On Windows that path produces a vault that initializes cleanly, passesecc memory doctor, and then refuses every write. The failure mode is confusing becauseinitanddoctorboth succeed.