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

Refusing unsafe native hook transaction backup ${backupPath}

Error message

Refusing unsafe native hook transaction backup ${backupPath}.

What it means

After opening the backup file exclusively ('wx', mode 0600), writing, fsyncing, and closing, setup lstats the final path. If it is a symlink, not a regular file, or has a hardlink count other than 1, the backup is considered unsafe and rejected — the backup must be a sole, regular, non-symlink file.

Source

Thrown at src/cli/setup.ts:2002

			} 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}.`);
		}
	}
	if (options.verbose) {
		console.log(`  backup ${artifact.path} -> ${backupPath}`);
	}
	return true;
}

async function commitNativeHookTransaction(
	artifacts: readonly NativeHookTransactionArtifact[],
	preconditions: readonly NativeHookTransactionPrecondition[],
	ancestorPrecondition: NativeHookTransactionAncestorPrecondition,
	backupContext: SetupBackupContext,
	tracker: RegularFileDurabilityTracker,

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Verify no hardlinks: check nlink of the backup file and remove external links
  2. Ensure the backup directory is not symlinked and not on exotic filesystems
  3. Re-run setup once the tree is clean
  4. Audit for processes that link or replace files under the backup root
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from "node:fs/promises";
const st = await lstat(backupPath).catch(() => null);
if (st && (st.isSymbolicLink() || !st.isFile() || st.nlink !== 1)) throw new Error(`unsafe backup: ${backupPath}`);

Try / catch

try { await createBackup(artifact); } catch (e) { if (e instanceof Error && e.message.includes("Refusing unsafe native hook transaction backup")) { /* remove hardlinks/symlinks at backupPath, retry */ } else throw e; }

Prevention

When it happens

Trigger: The written backupPath resolves to a symlink, a non-regular file, or is hardlinked elsewhere (nlink > 1) by the time of the post-write lstat.

Common situations: Hardlink attacks or accidents (backup dir on a filesystem with dedup), symlink planting races, or test injection substituting the file.

Related errors


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