paperclipai/paperclip · error · Error

Refusing to overwrite ${filePath} because it changed after t

Error message

Refusing to overwrite ${filePath} because it changed after the invalid backup was created

What it means

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.

Source

Thrown at cli/src/config/store.ts:214

  if (fs.existsSync(filePath)) {
    try {
      const source = paperclipConfigSchema.parse(migrateLegacyConfig(parseJson(filePath)));
      nextConfig = paperclipConfigSchema.parse(mergePaperclipConfig(source, nextConfig));
      if (isDeepStrictEqual(effectiveConfig(source), effectiveConfig(nextConfig))) {
        return false;
      }
    } catch (error) {
      const invalidBackupPath = options.invalidBackupPath;
      if (!invalidBackupPath) {
        throw new Error(
          `Refusing to overwrite invalid config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
        );
      }
      if (
        !fs.existsSync(invalidBackupPath) ||
        !fs.readFileSync(filePath).equals(fs.readFileSync(invalidBackupPath))
      ) {
        throw new Error(
          `Refusing to overwrite ${filePath} because it changed after the invalid backup was created`,
        );
      }
    }
  }

  // Backup existing config before overwriting
  if (fs.existsSync(filePath)) {
    const backupPath = filePath + ".backup";
    durableCopyFile(filePath, backupPath);
  }

  atomicWriteFile(filePath, JSON.stringify(nextConfig, null, 2) + "\n");
  return true;
}

export function configExists(configPath?: string): boolean {
  return fs.existsSync(resolveConfigPath(configPath));

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Re-run the backup step immediately before writeConfig() so the backup reflects the current bytes, then retry.
  2. Ensure no other process or editor is modifying config.json concurrently (close editors, serialize CLI invocations).
  3. Verify invalidBackupPath exists and contains the bytes you expect before calling writeConfig().
  4. If the current config is now valid (someone fixed it), drop the invalid-backup flow and call writeConfig() normally.

Example fix

// before: backup taken earlier, file changed since
const backup = backupInvalidConfig(path);
// ... time passes, file is edited ...
writeConfig(cfg, path, { invalidBackupPath: backup }); // throws

// after: take backup immediately before write
import { backupInvalidConfig, writeConfig } from "./config/store.js";
const backup = backupInvalidConfig(path);
writeConfig(cfg, path, { invalidBackupPath: backup }); // no gap
Defensive patterns

Strategy: validation

Validate before calling

import fs from "node:fs";

function backupMatchesCurrent(currentPath: string, backupPath: string): boolean {
  if (!fs.existsSync(backupPath) || !fs.existsSync(currentPath)) return false;
  return fs.readFileSync(currentPath).equals(fs.readFileSync(backupPath));
}

// Re-take backup immediately before writeConfig if mismatched

Type guard

import fs from "node:fs";

function isBackupFresh(currentPath: string, backupPath: string): boolean {
  try {
    return fs.readFileSync(currentPath).equals(fs.readFileSync(backupPath));
  } catch { return false; }
}

Try / catch

try {
  writeConfig(cfg, path, { invalidBackupPath: backup });
} catch (err) {
  if (err instanceof Error && err.message.includes("changed after the invalid backup was created")) {
    // re-take backup now and retry once
    const fresh = backupInvalidConfig(path);
    writeConfig(cfg, path, { invalidBackupPath: fresh });
  } else throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/a0b4b387ee5eda98. Report an issue: GitHub.