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

Refusing to use unsafe backup ancestor ${currentPath}.

Error message

Refusing to use unsafe backup ancestor ${currentPath}.

What it means

While walking each component of the backup path under the controlled root, setup lstats every existing ancestor. If an ancestor is a symlink or not a directory (e.g. a regular file in the middle of the path), it refuses to use it, preventing writes through symlinks that could redirect the backup outside the root.

Source

Thrown at src/cli/setup.ts:1982

	const bytes = artifact.before.bytes;
	if (bytes === null) return false;
	const backupPath = nativeHookTransactionBackupPath(artifact.path, backupContext);
	if (!options.dryRun) {
		const relativeParent = relative(backupContext.baseRoot, dirname(backupPath));
		if (
			isAbsolute(relativeParent) ||
			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);

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Replace symlinked ancestors with real directories (or point the whole baseRoot at the symlink target)
  2. Delete files that occupy directory positions in the backup path
  3. Re-run setup after normalizing the tree
  4. Keep the backup root free of symlinks by policy
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from "node:fs/promises";
for (const dir of ancestorDirs(backupRoot, relPath)) {
  const st = await lstat(dir).catch(() => null);
  if (st && (st.isSymbolicLink() || !st.isDirectory())) throw new Error(`unsafe ancestor: ${dir}`);
}

Try / catch

try { await createBackup(artifact); } catch (e) { if (e instanceof Error && e.message.startsWith("Refusing to use unsafe backup ancestor")) { /* replace symlink with real dir, retry */ } else throw e; }

Prevention

When it happens

Trigger: An intermediate directory in the backup path is a symbolic link, or a non-directory file occupies a path component that must be a directory.

Common situations: Users symlinking config directories (e.g. ~/.config/codex -> dotfiles repo), leftover files where directories are expected, or adversarial pre-seeded trees in tests.

Related errors


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