Hmbown/CodeWhale · error · io::Error

Codewhale-owned credential file must be singly linked…

Error message

Codewhale-owned credential file must be singly linked, owned by this user, and mode 0600 or stricter

What it means

For Codewhale-owned credential files the library enforces a hardening policy on the opened handle: exactly one hard link, owned by the effective UID, and no group/other permission bits (mode 0600 or stricter). It throws PermissionDenied when any of these fail, because a multi-linked, foreign-owned, or group/world-readable credential file can be read or swapped by other local users.

Solutions

  1. Fix ownership and mode: `chown $(id -u) <path> && chmod 600 <path>`.
  2. Remove extra hard links: `ls -li <path>` to find links, then delete or relocate the duplicates so nlink is 1 (or move the file to break the link).
  3. Recreate the file atomically with strict mode: write to a temp file with mode 0600 in the same directory and rename it over the path.
  4. Check sync/backup tooling and exclude the credential directory from hard-linking dedupe.

Example fix

// shell, before
ls -l ~/.codewhale/credentials  # -rw-r--r-- 2 root root
// after
sudo chown $(id -u) ~/.codewhale/credentials/token.json
chmod 600 ~/.codewhale/credentials/token.json
find ~ -inum $(stat -c %i ~/.codewhale/credentials/token.json)  # find and remove other links
Defensive patterns

Strategy: validation

Validate before calling

use std::os::unix::fs::MetadataExt;
fn ensure_owner_only_unix(path: &Path) -> std::io::Result<()> {
    let md = std::fs::metadata(path)?;
    if md.uid() != unsafe { libc::geteuid() } || md.mode() & 0o077 != 0 || md.nlink() != 1 {
        return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "credential must be 0600, singly linked, user-owned"));
    }
    Ok(())
}

Type guard

fn is_unix_owner_only(path: &Path) -> bool {
    use std::os::unix::fs::MetadataExt;
    std::fs::metadata(path).map(|md| {
        md.mode() & 0o077 == 0 && md.nlink() == 1
    }).unwrap_or(false)
}

Try / catch

match read_codewhale_owned_to_string(&path) {
    Ok(creds) => use(creds),
    Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
        eprintln!("fix with: chown $(id -u) {p} && chmod 600 {p}; check nlink with ls -li", p = path.display());
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: read_codewhale_owned_to_string opens a file where metadata.uid() != geteuid(), or mode & 0o077 != 0 (group/other bits set), or nlink() != 1 (hard-linked elsewhere).

Common situations: File was created with umask leaving mode 0644; a backup or sync tool (rsync hard links, git worktree, dedupe tools) hard-linked the file; the file was copied by root and left owned by root; a dotfile manager applied permissive modes.

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/877ba700e798ebc9. Report an issue: GitHub.

Appendix: source

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

            io::ErrorKind::InvalidInput,
            "external credential path must name a file",
        ));
    }
    let metadata = current.metadata()?;
    if !metadata.file_type().is_file() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "external credential path must name a regular file",
        ));
    }
    if require_owner_only {
        use std::os::unix::fs::MetadataExt as _;
        // SAFETY: geteuid(2) dereferences no pointers.
        if metadata.uid() != unsafe { libc::geteuid() }
            || metadata.mode() & 0o077 != 0
            || metadata.nlink() != 1
        {
            return Err(io::Error::new(
                io::ErrorKind::PermissionDenied,
                "Codewhale-owned credential file must be singly linked, owned by this user, and mode 0600 or stricter",
            ));
        }
    }
    Ok(current)
}

#[cfg(windows)]
fn open_secure_regular_file(path: &Path, require_owner_only: bool) -> io::Result<File> {
    use std::ffi::OsString;
    use std::os::windows::ffi::OsStringExt;
    use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
    use std::os::windows::io::AsRawHandle;
    use std::path::Component;
    use windows_sys::Win32::Storage::FileSystem::{
        FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_OPEN_REPARSE_POINT, FILE_NAME_OPENED,
        GetFinalPathNameByHandleW, VOLUME_NAME_DOS,

View on GitHub (pinned to 73e0f67d83)