affaan-m/ECC · error · Error
Refusing to : managed destination changed during the write.
Error message
Refusing to ${action}: managed destination changed during the write. What it means
Before writing a managed file, install-lifecycle.js pins the identity of the destination's parent directory so it can detect concurrent modification ('TOCTOU' protection). getStableParentStat lstats the parent directory and throws this error if the parent is not a directory or is itself a symlink — meaning the path layout changed between planning and writing, or was replaced by a symlink pointing elsewhere. The library refuses the write rather than following a possibly attacker-controlled symlink.
Solutions
- Re-run the install/repair command; the race is usually transient and a fresh run will re-snapshot a stable state.
- Inspect path.dirname(filePath): remove or replace whatever now occupies it if it is a file or an unintended symlink (e.g. ls -la to spot symlinks).
- Ensure no other installer, package manager, or file-sync process is mutating the target tree concurrently; serialize installs.
- If the destination is symlink-managed (dotfiles/stow), point the installer at the real directory or adjust its trusted-root configuration instead of letting it write through the symlink.
Defensive patterns
Strategy: retry
Validate before calling
const parent = path.dirname(destination);
let st;
try { st = fs.lstatSync(parent); } catch { throw new Error(`Parent missing before write: ${parent}`); }
if (!st.isDirectory() || st.isSymbolicLink()) {
throw new Error(`Refusing to write: ${parent} is not a real directory (file or symlink present)`);
} Try / catch
let attempts = 0;
while (attempts < 3) {
try { performManagedWrite(filePath); break; }
catch (err) {
if (!err.message.includes('managed destination changed')) throw err;
if (++attempts === 3) throw new Error(`Destination ${filePath} keeps changing; stop concurrent writers and retry.`);
await new Promise(r => setTimeout(r, 250 * attempts));
}
} Prevention
- Run only one installer instance at a time; use a lock file for CI or scheduled runs.
- Exclude managed install directories from file-sync tools (Dropbox, OneDrive, iCloud) and realtime backup agents.
- Avoid symlink-farm layouts (stow/dotfiles) for paths the installer manages, or configure its trusted root accordingly.
- Re-run the install after any manual restructuring of the target tree.
When it happens
Trigger: Calling the managed write path (via assertPinnedWriteDestination → getStableParentStat, reached from writeFileNoFollow) when path.dirname(filePath) is: (a) a regular file or other non-directory (e.g. someone created a file where a directory was expected), (b) a symlink to a directory. Requires that an earlier expectedParentStat snapshot existed and the live stat now differs or is invalid.
Common situations: Another process (editor, sync tool like Dropbox/OneDrive, a parallel install) replaced or removed directories under the install target while the installer ran; a symlink farm or relocated install target (e.g. ~/.claude symlinked to a dotfiles-managed directory); running the installer twice concurrently; antivirus or backup software touching paths mid-write.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Invalid ECC repo root: unreadable package.json at
- Refusing to read a file that changed during validation
- artifact byte count exceeded during reading
- artifact changed before reading
- artifact changed during reading
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/f5ab2d70c636ec62.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/install-lifecycle.js:454
{ allowFinalSymlink }
);
return finalDestination.exists ? finalDestination.managedPath : null;
}
function hasSameFileIdentity(leftStat, rightStat) {
return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
}
function createChangedDestinationError(action) {
return new Error(
`Refusing to ${action}: managed destination changed during the write.`
);
}
function getStableParentStat(filePath, action) {
const parentStat = fs.lstatSync(path.dirname(filePath));
if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) {
throw createChangedDestinationError(action);
}
return parentStat;
}
function assertPinnedWriteDestination(
filePath,
fileDescriptor,
expectedParentStat,
trustedRoot,
action
) {
const liveDestination = getManagedDestination(filePath, trustedRoot, action);
if (path.resolve(liveDestination.managedPath) !== path.resolve(filePath)) {
throw createChangedDestinationError(action);
}
const liveParentStat = getStableParentStat(filePath, action);
if (!hasSameFileIdentity(expectedParentStat, liveParentStat)) {View on GitHub (pinned to 8321021c54)