Hmbown/CodeWhale · error · anyhow::Error
Automation lock must not have hard links
Error message
Automation lock must not have hard links
What it means
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.
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
Example fix
// shell // before ln ~/.app/state/automation.lock ~/backup/automation.lock # nlink=2, rejected // after ln -s ~/.app/state/automation.lock ~/backup/automation.lock # symlink does not raise nlink
Defensive patterns
Strategy: validation
Validate before calling
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let n = std::fs::metadata(&lock_path)?.nlink();
assert_eq!(n, 1, "lock file must not be hard-linked");
} Try / catch
if err.to_string().contains("must not have hard links") {
std::fs::remove_file(&lock_path).ok(); // recreate as a fresh single-link file
} Prevention
- 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>
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Automation lock must not be a reparse point
- Codewhale-owned credential file must be singly linked…
- private sidecar file
- private sidecar file
- refusing a symlinked Runtime Chat owner lock
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/e200955f80186869.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/automation_manager.rs:1029
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt as _;
options.custom_flags(0x0020_0000); // FILE_FLAG_OPEN_REPARSE_POINT
}
let file = options
.open(&path)
.with_context(|| format!("open {}", path.display()))?;
let metadata = file.metadata()?;
if !metadata.is_file() {
bail!("Automation lock must be a regular file");
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt as _;
if metadata.nlink() != 1 {
bail!("Automation lock must not have hard links");
}
}
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt as _;
if metadata.file_attributes() & 0x400 != 0 {
bail!("Automation lock must not be a reparse point");
}
}
Ok(fd_lock::RwLock::new(file))
}
fn with_transaction<T>(&self, operation: impl FnOnce() -> Result<T>) -> Result<T> {
let mut lock = self.open_lock("state.lock")?;
let _guard = lock.write().context("lock automation state")?;
operation()
}
View on GitHub (pinned to 73e0f67d83)