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

  1. Retry daemon startup once — the next attempt creates a fresh lock.
  2. Find and stop whatever unlinks the lock: check systemd-tmpfiles rules, cron jobs, and operator habits.
  3. Never delete <socket>.lock manually; it is persistent by design.
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/a4c1b6ee8a58c7b1. Report an issue: GitHub.