zeroclaw-labs/zeroclaw · error
local IPC endpoint lock {} was unlinked while being opened
Error message
local IPC endpoint lock {} was unlinked while being opened What it means
After opening the lock file, its link count (nlink) was 0 — the file was unlinked between open and fstat. With no directory entry, the dev/inode identity check cannot guarantee that two daemons see the same lock inode, so ZeroClaw refuses to proceed. This is a transient race, typically caused by cleanup tooling deleting the persistent lock (the lock is intentionally never unlinked by ZeroClaw itself).
Source
Thrown at crates/zeroclaw-runtime/src/rpc/local.rs:350
let euid = unsafe { libc::geteuid() };
if metadata.uid() != euid {
anyhow::bail!(
"local IPC endpoint lock {} is owned by uid {}, not the \
daemon user; remove it or choose a different socket path",
lock_path.display(),
metadata.uid()
);
}
if metadata.mode() & 0o077 != 0 {
anyhow::bail!(
"local IPC endpoint lock {} is accessible to other users \
(mode {:o}); restrict it to 0600 or remove it",
lock_path.display(),
metadata.mode() & 0o7777
);
}
if metadata.nlink() == 0 {
anyhow::bail!(
"local IPC endpoint lock {} was unlinked while being opened",
lock_path.display()
);
}
Ok(())
}
impl EndpointLock {
pub(super) fn acquire(path: &Path) -> Result<Self> {
let mut lock_name = path.as_os_str().to_os_string();
lock_name.push(".lock");
let lock_path = PathBuf::from(lock_name);
require_trusted_lock_dir(&lock_path)?;
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.mode(0o600)View on GitHub (pinned to 88bb9c8533)
Solutions
- Retry daemon startup once — the next attempt creates a fresh lock.
- Find and stop whatever unlinks the lock: check systemd-tmpfiles rules, cron jobs, and operator habits.
- Never delete <socket>.lock manually; it is persistent by design.
- If on NFS, move the socket to a local filesystem.
Defensive patterns
Strategy: retry
Try / catch
// transient unlink race: retry acquisition once before surfacing the error
let lock = match acquire_with_retry(&sock_path, 1) {
Ok(l) => l,
Err(e) if e.to_string().contains("was unlinked") => { /* report cleanup-tool interference */ return Err(e) }
Err(e) => return Err(e),
}; Prevention
- Exclude the socket dir from tmpfiles.d/cron cleanup (or use Age= with sticky persistence).
- Document that <socket>.lock is persistent by design and must never be deleted.
- Avoid NFS for lock files; use a local filesystem.
When it happens
Trigger: A tmpfiles.d/cron/systemd-tmpfiles rule deletes '*.lock' or the socket dir contents while the daemon starts; an operator manually rm'ed the lock at the same moment; NFS silly-rename semantics dropping the link; a container restart wiping a directory that is actually a bind-mounted file.
Common situations: systemd-tmpfiles cleans /run every boot and races a fast-starting daemon; aggressive /tmp cleaners; 'cleanup' scripts written before the lock was made persistent.
Related errors
- local IPC endpoint lock {} is not a regular file; remove it
- local IPC endpoint lock {} was replaced while being acquired
- local IPC endpoint lock directory {} is owned by uid {}; it
- local IPC endpoint lock {} is owned by uid {}, not the daemo
- local IPC endpoint lock {} is accessible to other users (mod
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/a4c1b6ee8a58c7b1.
Report an issue: GitHub.