fix(install): pin Claude settings parent during atomic replacement

Reject directory replacement after temporary file creation or staging, preserve unrelated files during cleanup, and retry settings edits observed before the final rename. Add three regression tests for the review findings.
This commit is contained in:
haelyra
2026-09-07 16:31:33 -04:00
parent f59cfd57c2
commit dbe8bfbba9
3 changed files with 131 additions and 2 deletions
+14 -1
View File
@@ -13,21 +13,34 @@ function writeFileAtomic(filePath, content, options = {}) {
);
const mode = options.mode || 0o600;
if (options.validateParent) options.validateParent();
fs.mkdirSync(parentDir, { recursive: true });
let descriptor;
try {
if (options.validateParent) options.validateParent();
descriptor = fs.openSync(tempPath, 'wx', mode);
if (options.validateParent) options.validateParent();
fs.writeFileSync(descriptor, content, { encoding: options.encoding || 'utf8' });
fs.fsyncSync(descriptor);
fs.closeSync(descriptor);
descriptor = undefined;
if (options.validateParent) options.validateParent();
if (options.beforeRename) options.beforeRename();
fs.renameSync(tempPath, resolvedPath);
} catch (error) {
if (descriptor !== undefined) {
fs.closeSync(descriptor);
}
fs.rmSync(tempPath, { force: true });
// If the parent was replaced, this pathname may now name somebody else's
// file. Leave the private staging file in its original directory.
let parentUnchanged = true;
try {
if (options.validateParent) options.validateParent();
} catch (_error) {
parentUnchanged = false;
}
if (parentUnchanged) fs.rmSync(tempPath, { force: true });
throw error;
}