{"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":"anyhow::Error","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/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/automation_manager.rs#L1011-L1047","documentation":"On Unix, after confirming the lock file is regular, the manager checks metadata.nlink() == 1. Multiple hard links to the lock file break the assumption that holding the fd lock protects the unique lock resource (another process could swap paths), so any file with nlink != 1 is rejected.","triggerScenarios":"The lock file was hard-linked elsewhere (ln without -s), or a backup/sync tool created hard links at the lock path before the manager opened it.","commonSituations":"Backup tools (rsync --link-dest, git worktree-style dedup) hard-linking state directories; a user manually hard-linking to 'back up' the lock; scripts that link state files into shared caches.","solutions":["Delete the extra hard link so the lock path has nlink == 1 (find links via find -samefile), then retry","Remove the lock file entirely and let the manager recreate it","Exclude the state/lock directory from hard-link-based backup/sync jobs"],"exampleFix":"// shell\n// before\nln ~/.app/state/automation.lock ~/backup/automation.lock  # nlink=2, rejected\n// after\nln -s ~/.app/state/automation.lock ~/backup/automation.lock  # symlink does not raise nlink","handlingStrategy":"validation","validationCode":"#[cfg(unix)]\n{\n    use std::os::unix::fs::MetadataExt;\n    let n = std::fs::metadata(&lock_path)?.nlink();\n    assert_eq!(n, 1, \"lock file must not be hard-linked\");\n}","typeGuard":null,"tryCatchPattern":"if err.to_string().contains(\"must not have hard links\") {\n    std::fs::remove_file(&lock_path).ok(); // recreate as a fresh single-link file\n}","preventionTips":["Never hard-link state or lock files for backup — copy instead","Exclude lock/state directories from hard-link-based backup jobs (rsync --link-dest)","Find stray links with: find <dir> -samefile <lockpath>"],"tags":["filesystem","lock","unix","security"],"backgroundTag":"invalid-argument-value","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}