{"record":{"id":"e200955f80186869","repo":"Hmbown/CodeWhale","slug":"automation-lock-must-not-have-hard-links","errorCode":null,"errorMessage":"Automation lock must not have hard links","messagePattern":"Automation lock must not have hard links","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/automation_manager.rs","lineNumber":1029,"sourceCode":"                .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);\n        }\n        #[cfg(windows)]\n        {\n            use std::os::windows::fs::OpenOptionsExt as _;\n            options.custom_flags(0x0020_0000); // FILE_FLAG_OPEN_REPARSE_POINT\n        }\n        let file = options\n            .open(&path)\n            .with_context(|| format!(\"open {}\", path.display()))?;\n        let metadata = file.metadata()?;\n        if !metadata.is_file() {\n            bail!(\"Automation lock must be a regular file\");\n        }\n        #[cfg(unix)]\n        {\n            use std::os::unix::fs::MetadataExt as _;\n            if metadata.nlink() != 1 {\n                bail!(\"Automation lock must not have hard links\");\n            }\n        }\n        #[cfg(windows)]\n        {\n            use std::os::windows::fs::MetadataExt as _;\n            if metadata.file_attributes() & 0x400 != 0 {\n                bail!(\"Automation lock must not be a reparse point\");\n            }\n        }\n        Ok(fd_lock::RwLock::new(file))\n    }\n\n    fn with_transaction<T>(&self, operation: impl FnOnce() -> Result<T>) -> Result<T> {\n        let mut lock = self.open_lock(\"state.lock\")?;\n        let _guard = lock.write().context(\"lock automation state\")?;\n        operation()\n    }\n","sourceCodeStart":1011,"sourceCodeEnd":1047,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/tui/src/automation_manager.rs#L1011-L1047","documentation":"On Unix, after confirming the lock path is a regular file, the manager checks its hard-link count (st_nlink). A lock file with nlink != 1 means additional directory entries reference the same inode, which breaks the lock's uniqueness assumptions (another path could bypass or interfere with locking), so it bails.","triggerScenarios":"Someone ran `ln <lockfile> <other-path>` creating a second hard link to the lock file, or a backup/sync tool hard-linked the lock.","commonSituations":"Dotfile managers (stow, rclone, backup restore) that hard-link files, users creating aliases to the lock file to 'share' state, or restores that re-link files.","solutions":["Remove the extra hard links: find all links via `find -samefile <lockpath>` and delete the duplicates so nlink becomes 1","Recreate the lock file: delete it and let the automation manager recreate it with a single link","Configure backup/sync tools to copy instead of hard-link lock files"],"exampleFix":"// before\n$ ln /run/app/automation.lock /run/app/automation.lock.bak   (nlink=2)\n// after\n$ rm /run/app/automation.lock.bak                            (nlink=1)","handlingStrategy":"validation","validationCode":"#[cfg(unix)]\nfn nlink_is_one(p: &std::path::Path) -> bool {\n    use std::os::unix::fs::MetadataExt;\n    std::fs::metadata(p).map(|m| m.nlink() == 1).unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"match acquire_automation_lock(path) {\n    Ok(g) => run(g),\n    Err(e) => {\n        eprintln!(\"Lock hard-linked: {e}; deleting lock and recreating\");\n        let _ = std::fs::remove_file(path);\n        // retry acquisition once\n    }\n}","preventionTips":["Never `ln` a lock file to another path; use symlinks absent or nothing at all","Exclude lock files from backup/sync tools configured to hard-link","Audit with `find -samefile <lockpath>` if you suspect duplicate links"],"tags":["filesystem","locking","unix","hardlink"],"backgroundTag":"internal-invariant-violation","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}