{"record":{"id":"5bc1b8fa03383f60","repo":"Yeachan-Heo/oh-my-codex","slug":"refusing-to-use-unsafe-backup-ancestor-currentpa","errorCode":null,"errorMessage":"Refusing to use unsafe backup ancestor ${currentPath}.","messagePattern":"Refusing to use unsafe backup ancestor (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/cli/setup.ts","lineNumber":1982,"sourceCode":"\tconst bytes = artifact.before.bytes;\n\tif (bytes === null) return false;\n\tconst backupPath = nativeHookTransactionBackupPath(artifact.path, backupContext);\n\tif (!options.dryRun) {\n\t\tconst relativeParent = relative(backupContext.baseRoot, dirname(backupPath));\n\t\tif (\n\t\t\tisAbsolute(relativeParent) ||\n\t\t\trelativeParent === \"..\" ||\n\t\t\trelativeParent.startsWith(`..${sep}`)\n\t\t) {\n\t\t\tthrow new Error(`Refusing to back up ${artifact.path} outside controlled backup root.`);\n\t\t}\n\t\tlet currentPath = backupContext.baseRoot;\n\t\tfor (const component of relativeParent.split(sep).filter(Boolean)) {\n\t\t\tcurrentPath = join(currentPath, component);\n\t\t\ttry {\n\t\t\t\tconst currentStat = await lstat(currentPath);\n\t\t\t\tif (currentStat.isSymbolicLink() || !currentStat.isDirectory()) {\n\t\t\t\t\tthrow new Error(`Refusing to use unsafe backup ancestor ${currentPath}.`);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tif (!isMissingPathError(error)) throw error;\n\t\t\t\tawait mkdir(currentPath);\n\t\t\t\tconst createdStat = await lstat(currentPath);\n\t\t\t\tif (createdStat.isSymbolicLink() || !createdStat.isDirectory()) {\n\t\t\t\t\tthrow new Error(`Refusing to use unsafe created backup ancestor ${currentPath}.`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst handle = await open(backupPath, \"wx\", 0o600);\n\t\ttry {\n\t\t\tawait handle.writeFile(bytes);\n\t\t\trecordRegularFileSyncOutcome(tracker, await syncNativeHookRegularFile(handle));\n\t\t} finally {\n\t\t\tawait handle.close();\n\t\t}\n\t\tconst backupStat = await lstat(backupPath);","sourceCodeStart":1964,"sourceCodeEnd":2000,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/cli/setup.ts#L1964-L2000","documentation":"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.","triggerScenarios":"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.","commonSituations":"Users symlinking config directories (e.g. ~/.config/codex -> dotfiles repo), leftover files where directories are expected, or adversarial pre-seeded trees in tests.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import { lstat } from \"node:fs/promises\";\nfor (const dir of ancestorDirs(backupRoot, relPath)) {\n  const st = await lstat(dir).catch(() => null);\n  if (st && (st.isSymbolicLink() || !st.isDirectory())) throw new Error(`unsafe ancestor: ${dir}`);\n}","typeGuard":null,"tryCatchPattern":"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; }","preventionTips":["Keep the backup root free of symlinks","Materialize symlinked config dirs as real directories","Check trees with lstat (not stat) so symlinks are detected"],"tags":["symlink","security","backup","filesystem"],"backgroundTag":"symlink-escape-blocked","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}