Hmbown/CodeWhale · error

provider catalog lock

Error message

provider catalog lock {} must not be hard linked

What it means

On Unix, the provider catalog lock file must have an nlink count of 1. If it is hard linked elsewhere, another process could swap the underlying inode and bypass the advisory lock, so locking is refused as a security/integrity guard.

Solutions

  1. Delete the lock file (`rm <path>`) and retry; it is recreated with a single link.
  2. Find the other links with `find / -samefile <path> 2>/dev/null` and remove them or exclude the cache dir from dedup tools.
  3. Reconfigure backup/dedup tooling to skip the codewhale cache directory.
  4. Restore the cache directory from a clean state if links keep reappearing.

Example fix

// before: lock has 2 links
$ stat -c %h ~/.cache/codewhale/provider-catalog.lock
2
// after
$ rm ~/.cache/codewhale/provider-catalog.lock
$ stat -c %h ~/.cache/codewhale/provider-catalog.lock  # recreated as 1
Defensive patterns

Strategy: validation

Validate before calling

import { statSync } from 'node:fs';
if (process.platform !== 'win32') {
  const st = statSync(lockPath);
  if (st.nlink > 1) {
    console.error(`${lockPath} is hard linked ${st.nlink}x — remove before running`);
    process.exit(1);
  }
}

Type guard

const hasSingleLink = (p) => { try { return statSync(p).nlink === 1; } catch { return false; } };

Try / catch

try {
  await codewhale.providers.refresh();
} catch (e) {
  if (e.message.includes('must not be hard linked')) {
    fs.rmSync(lockPath, { force: true });
    return codewhale.providers.refresh();
  }
  throw e;
}

Prevention

When it happens

Trigger: `open_cache_lock` (called by `load_from_disk`, `persist_scope`, `persist_failure_scope`) on Unix sees `metadata.nlink() != 1` — the lock file has additional hard links.

Common situations: Backup/dedup tools (rsync --link-dest, git-annex, content-addressed stores) hard-linked the cache file, a user hard-linked the lock across directories, or a snapshot/restore tool relinked cache files.

Understand the failure class

Background: "open() failed", "failed to open file", "cannot create file" — what a file open error means and how to fix it — this error's family across 42 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/f9657533a24e6729. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/provider_catalog_live.rs:545

    {
        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 provider catalog lock {}", path.display()))?;
    let metadata = file
        .metadata()
        .with_context(|| format!("inspect provider catalog lock {}", path.display()))?;
    anyhow::ensure!(
        metadata.is_file(),
        "provider catalog lock {} must be a regular file",
        path.display()
    );
    #[cfg(unix)]
    {
        use std::os::unix::fs::MetadataExt as _;
        anyhow::ensure!(
            metadata.nlink() == 1,
            "provider catalog lock {} must not be hard linked",
            path.display()
        );
    }
    #[cfg(windows)]
    {
        use std::os::windows::fs::MetadataExt as _;
        const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
        anyhow::ensure!(
            metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT == 0,
            "provider catalog lock {} must not be a reparse point",
            path.display()
        );
    }
    Ok(file)
}

View on GitHub (pinned to 73e0f67d83)