fix: write clawdbot config atomically

This commit is contained in:
Peter Steinberger
2026-01-09 21:27:37 +01:00
parent 564caf49bb
commit 2bb9716598

View File

@@ -1,3 +1,4 @@
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
@@ -295,13 +296,48 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
}
async function writeConfigFile(cfg: ClawdbotConfig) {
await deps.fs.promises.mkdir(path.dirname(configPath), {
recursive: true,
});
const dir = path.dirname(configPath);
await deps.fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
const json = JSON.stringify(applyModelDefaults(cfg), null, 2)
.trimEnd()
.concat("\n");
await deps.fs.promises.writeFile(configPath, json, "utf-8");
const tmp = path.join(
dir,
`${path.basename(configPath)}.${process.pid}.${crypto.randomUUID()}.tmp`,
);
await deps.fs.promises.writeFile(tmp, json, {
encoding: "utf-8",
mode: 0o600,
});
await deps.fs.promises
.copyFile(configPath, `${configPath}.bak`)
.catch(() => {
// best-effort
});
try {
await deps.fs.promises.rename(tmp, configPath);
} catch (err) {
const code = (err as { code?: string }).code;
// Windows doesn't reliably support atomic replace via rename when dest exists.
if (code === "EPERM" || code === "EEXIST") {
await deps.fs.promises.copyFile(tmp, configPath);
await deps.fs.promises.chmod(configPath, 0o600).catch(() => {
// best-effort
});
await deps.fs.promises.unlink(tmp).catch(() => {
// best-effort
});
return;
}
await deps.fs.promises.unlink(tmp).catch(() => {
// best-effort
});
throw err;
}
}
return {