{"record":{"id":"d820db9b3ca503e2","repo":"Yeachan-Heo/oh-my-codex","slug":"refusing-unsafe-native-hook-transaction-backup-b","errorCode":null,"errorMessage":"Refusing unsafe native hook transaction backup ${backupPath}.","messagePattern":"Refusing unsafe native hook transaction backup (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/cli/setup.ts","lineNumber":2002,"sourceCode":"\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);\n\t\tif (backupStat.isSymbolicLink() || !backupStat.isFile() || backupStat.nlink !== 1) {\n\t\t\tthrow new Error(`Refusing unsafe native hook transaction backup ${backupPath}.`);\n\t\t}\n\t\tconst writtenBytes = await readFile(backupPath);\n\t\tif (!writtenBytes.equals(bytes)) {\n\t\t\tthrow new Error(`Native hook transaction backup verification failed for ${backupPath}.`);\n\t\t}\n\t}\n\tif (options.verbose) {\n\t\tconsole.log(`  backup ${artifact.path} -> ${backupPath}`);\n\t}\n\treturn true;\n}\n\nasync function commitNativeHookTransaction(\n\tartifacts: readonly NativeHookTransactionArtifact[],\n\tpreconditions: readonly NativeHookTransactionPrecondition[],\n\tancestorPrecondition: NativeHookTransactionAncestorPrecondition,\n\tbackupContext: SetupBackupContext,\n\ttracker: RegularFileDurabilityTracker,","sourceCodeStart":1984,"sourceCodeEnd":2020,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/cli/setup.ts#L1984-L2020","documentation":"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.","triggerScenarios":"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.","commonSituations":"Hardlink attacks or accidents (backup dir on a filesystem with dedup), symlink planting races, or test injection substituting the file.","solutions":["Verify no hardlinks: check nlink of the backup file and remove external links","Ensure the backup directory is not symlinked and not on exotic filesystems","Re-run setup once the tree is clean","Audit for processes that link or replace files under the backup root"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import { lstat } from \"node:fs/promises\";\nconst st = await lstat(backupPath).catch(() => null);\nif (st && (st.isSymbolicLink() || !st.isFile() || st.nlink !== 1)) throw new Error(`unsafe backup: ${backupPath}`);","typeGuard":null,"tryCatchPattern":"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; }","preventionTips":["Avoid hardlinking backup files","Disable filesystem dedup features on the backup volume","Monitor nlink on sensitive config backups"],"tags":["symlink","hardlink","security","backup","file-integrity"],"backgroundTag":"symlink-escape-blocked","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}