{"record":{"id":"a0b4b387ee5eda98","repo":"paperclipai/paperclip","slug":"refusing-to-overwrite-filepath-because-it-chang","errorCode":null,"errorMessage":"Refusing to overwrite ${filePath} because it changed after the invalid backup was created","messagePattern":"Refusing to overwrite (.+?) because it changed after the invalid backup was created","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"cli/src/config/store.ts","lineNumber":214,"sourceCode":"  if (fs.existsSync(filePath)) {\n    try {\n      const source = paperclipConfigSchema.parse(migrateLegacyConfig(parseJson(filePath)));\n      nextConfig = paperclipConfigSchema.parse(mergePaperclipConfig(source, nextConfig));\n      if (isDeepStrictEqual(effectiveConfig(source), effectiveConfig(nextConfig))) {\n        return false;\n      }\n    } catch (error) {\n      const invalidBackupPath = options.invalidBackupPath;\n      if (!invalidBackupPath) {\n        throw new Error(\n          `Refusing to overwrite invalid config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`,\n        );\n      }\n      if (\n        !fs.existsSync(invalidBackupPath) ||\n        !fs.readFileSync(filePath).equals(fs.readFileSync(invalidBackupPath))\n      ) {\n        throw new Error(\n          `Refusing to overwrite ${filePath} because it changed after the invalid backup was created`,\n        );\n      }\n    }\n  }\n\n  // Backup existing config before overwriting\n  if (fs.existsSync(filePath)) {\n    const backupPath = filePath + \".backup\";\n    durableCopyFile(filePath, backupPath);\n  }\n\n  atomicWriteFile(filePath, JSON.stringify(nextConfig, null, 2) + \"\\n\");\n  return true;\n}\n\nexport function configExists(configPath?: string): boolean {\n  return fs.existsSync(resolveConfigPath(configPath));","sourceCodeStart":196,"sourceCodeEnd":232,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/cli/src/config/store.ts#L196-L232","documentation":"Thrown by writeConfig() inside the invalid-config recovery path: the caller DID supply options.invalidBackupPath, but the current file on disk no longer matches the backup's bytes (fs.readFileSync(filePath).equals(fs.readFileSync(invalidBackupPath)) is false), or the backup file itself is missing. This guards against a TOCTOU race where the config changed between the backup being made and the overwrite, which could cause silent data loss.","triggerScenarios":"Sequence: (1) backupInvalidConfig() copied the bad config to a .invalid-N file. (2) Something — another process, manual edit, another CLI invocation — modified the original config.json. (3) writeConfig(config, path, { invalidBackupPath }) is called; the byte-equality check fails. Also fires if invalidBackupPath was deleted between steps.","commonSituations":"1) Two concurrent CLI processes both writing config. 2) User manually edited config.json after the automated backup was captured. 3) The backup path points at a stale or wrong file. 4) An editor with auto-save touched the file.","solutions":["Re-run the backup step immediately before writeConfig() so the backup reflects the current bytes, then retry.","Ensure no other process or editor is modifying config.json concurrently (close editors, serialize CLI invocations).","Verify invalidBackupPath exists and contains the bytes you expect before calling writeConfig().","If the current config is now valid (someone fixed it), drop the invalid-backup flow and call writeConfig() normally."],"exampleFix":"// before: backup taken earlier, file changed since\nconst backup = backupInvalidConfig(path);\n// ... time passes, file is edited ...\nwriteConfig(cfg, path, { invalidBackupPath: backup }); // throws\n\n// after: take backup immediately before write\nimport { backupInvalidConfig, writeConfig } from \"./config/store.js\";\nconst backup = backupInvalidConfig(path);\nwriteConfig(cfg, path, { invalidBackupPath: backup }); // no gap","handlingStrategy":"validation","validationCode":"import fs from \"node:fs\";\n\nfunction backupMatchesCurrent(currentPath: string, backupPath: string): boolean {\n  if (!fs.existsSync(backupPath) || !fs.existsSync(currentPath)) return false;\n  return fs.readFileSync(currentPath).equals(fs.readFileSync(backupPath));\n}\n\n// Re-take backup immediately before writeConfig if mismatched","typeGuard":"import fs from \"node:fs\";\n\nfunction isBackupFresh(currentPath: string, backupPath: string): boolean {\n  try {\n    return fs.readFileSync(currentPath).equals(fs.readFileSync(backupPath));\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  writeConfig(cfg, path, { invalidBackupPath: backup });\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"changed after the invalid backup was created\")) {\n    // re-take backup now and retry once\n    const fresh = backupInvalidConfig(path);\n    writeConfig(cfg, path, { invalidBackupPath: fresh });\n  } else throw err;\n}","preventionTips":["Take the invalid backup immediately before writeConfig with no intervening operations.","Serialize config writes to avoid concurrent modifications.","Close auto-saving editors on config.json before running CLI write operations.","Verify backup byte-equality yourself before calling writeConfig when in doubt."],"tags":["config","toctou","race-condition","safety-guard","cli"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}