Hmbown/CodeWhale · error · io::Error

Codewhale-owned credential file must be singly linked

Error message

Codewhale-owned credential file must be singly linked

What it means

For Codewhale-owned credentials on Windows, the library queries GetFileInformationByHandle and requires nNumberOfLinks == 1, i.e. the file must have no other hard links. It throws PermissionDenied otherwise, because a second hard link lets another local process hold an alternate name for the secret that survives permission checks on the primary path.

Solutions

  1. Delete the extra hard links (`fsutil hardlink list <file>` to find them), leaving exactly one name.
  2. Recreate the credential as a fresh single file: write a new file with restrictive ACLs and update the configuration.
  3. Exclude the credential directory from deduplicating backup tools.
  4. Never share the credential by hard link; grant access via ACLs instead.

Example fix

// cmd, before
mklink /H C:\tools\token.json C:\Users\me\.codewhale\token.json
// after
del C:\tools\token.json
fsutil hardlink list C:\Users\me\.codewhale\token.json   // should show a single link
Defensive patterns

Strategy: validation

Validate before calling

// cmd pre-check: fsutil hardlink list <file>   — must print exactly one path

Try / catch

match read_codewhale_owned_to_string(&path) {
    Ok(creds) => use(creds),
    Err(e) if e.to_string().contains("singly linked") => {
        eprintln!("remove extra hard links: fsutil hardlink list {}", path.display());
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: read_codewhale_owned_to_string opens a Windows file whose link count is greater than 1, typically because `mklink /H` or `fsutil hardlink create` made an alias, or a copy tool created hard links.

Common situations: Backup/snapshot tools (dedupe, Storage Spaces, third-party backup) hard-link the credential file; a user created a hard link to share the token with another app; a container bind of the same underlying file shows extra links.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

Thrown at crates/tui/src/external_credentials.rs:331

    let expected = normalize_windows_path_for_comparison(path)?;
    if actual != expected {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            "external credential path was redirected while opening",
        ));
    }
    if require_owner_only {
        use windows_sys::Win32::Storage::FileSystem::{
            BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle,
        };
        let mut information = BY_HANDLE_FILE_INFORMATION::default();
        // SAFETY: the opened credential handle and output pointer remain valid
        // for the duration of the call.
        if unsafe { GetFileInformationByHandle(handle, &mut information) } == 0 {
            return Err(io::Error::last_os_error());
        }
        if information.nNumberOfLinks != 1 {
            return Err(io::Error::new(
                io::ErrorKind::PermissionDenied,
                "Codewhale-owned credential file must be singly linked",
            ));
        }
        verify_windows_owner_only_handle(handle)?;
    }
    Ok(file)
}

/// Normalize a Windows path without replacement characters. Unpaired UTF-16
/// is rejected so two distinct paths can never compare equal after a lossy
/// conversion. This is intentionally stricter than filesystem display.
#[cfg(windows)]
fn normalize_windows_path_for_comparison(path: &Path) -> io::Result<String> {
    let text = path.to_str().ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::PermissionDenied,
            "credential path contains invalid Unicode and cannot be compared safely",

View on GitHub (pinned to 73e0f67d83)