Yeachan-Heo/oh-my-codex · error · Error

Refusing to use unsafe created backup ancestor ${currentPath

Error message

Refusing to use unsafe created backup ancestor ${currentPath}.

What it means

When an ancestor directory of the backup path does not exist, setup mkdirs it and then lstats the freshly created directory. If the created entry is a symlink or not a directory (possible via races or pre-existing entries appearing between checks), the write is aborted.

Source

Thrown at src/cli/setup.ts:1989

			relativeParent === ".." ||
			relativeParent.startsWith(`..${sep}`)
		) {
			throw new Error(`Refusing to back up ${artifact.path} outside controlled backup root.`);
		}
		let currentPath = backupContext.baseRoot;
		for (const component of relativeParent.split(sep).filter(Boolean)) {
			currentPath = join(currentPath, component);
			try {
				const currentStat = await lstat(currentPath);
				if (currentStat.isSymbolicLink() || !currentStat.isDirectory()) {
					throw new Error(`Refusing to use unsafe backup ancestor ${currentPath}.`);
				}
			} catch (error) {
				if (!isMissingPathError(error)) throw error;
				await mkdir(currentPath);
				const createdStat = await lstat(currentPath);
				if (createdStat.isSymbolicLink() || !createdStat.isDirectory()) {
					throw new Error(`Refusing to use unsafe created backup ancestor ${currentPath}.`);
				}
			}
		}
		const handle = await open(backupPath, "wx", 0o600);
		try {
			await handle.writeFile(bytes);
			recordRegularFileSyncOutcome(tracker, await syncNativeHookRegularFile(handle));
		} finally {
			await handle.close();
		}
		const backupStat = await lstat(backupPath);
		if (backupStat.isSymbolicLink() || !backupStat.isFile() || backupStat.nlink !== 1) {
			throw new Error(`Refusing unsafe native hook transaction backup ${backupPath}.`);
		}
		const writtenBytes = await readFile(backupPath);
		if (!writtenBytes.equals(bytes)) {
			throw new Error(`Native hook transaction backup verification failed for ${backupPath}.`);
		}

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Ensure no other process mutates the backup root during setup
  2. Remove the offending entry at currentPath and retry
  3. Serialize setup runs with a lock
  4. Run setup with exclusive access to the backup tree (single-user, single-process)
Defensive patterns

Strategy: retry

Validate before calling

import { lstat } from "node:fs/promises";
const st = await lstat(currentPath);
if (st.isSymbolicLink() || !st.isDirectory()) throw new Error(`unsafe created ancestor: ${currentPath}`);

Try / catch

try { await createBackup(artifact); } catch (e) { if (e instanceof Error && e.message.includes("unsafe created backup ancestor")) { await rm(currentPath); await retry(createBackup, artifact); } else throw e; }

Prevention

When it happens

Trigger: mkdir(currentPath) succeeds but a concurrent process swaps the new directory for a symlink before lstat, or the entry already exists as a non-directory on platforms where mkdir is not exclusive.

Common situations: Race conditions with parallel setup runs or symlink-planting scenarios; rarely hit outside adversarial or heavily concurrent environments.

Related errors


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/8d02f44addcd5ad9. Report an issue: GitHub.