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
- Replace symlinked ancestors with real directories (or point the whole baseRoot at the symlink target)
- Delete files that occupy directory positions in the backup path
- Re-run setup after normalizing the tree
- 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
- Keep the backup root free of symlinks
- Materialize symlinked config dirs as real directories
- Check trees with lstat (not stat) so symlinks are detected
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
- Refusing cancellation through non-regular run state target:
- Refusing cancellation through non-regular state target ${ref
- Refusing to back up ${artifact.path} outside controlled back
- Refusing to use unsafe created backup ancestor ${currentPath
- Refusing unsafe native hook transaction backup ${backupPath}
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/5bc1b8fa03383f60.
Report an issue: GitHub.