Hmbown/CodeWhale · error · io::Error

external credential path must be absolute

Error message

external credential path must be absolute

What it means

`open_secure_regular_file` is the hardened opener used to read external credentials. Before opening anything it requires the path to be absolute; relative paths are rejected up-front with this `InvalidInput` error so the subsequent component-by-component `openat` walk rooted at `/` is well-defined and cannot be influenced by the process's current working directory.

Solutions

  1. Provide the credential path as an absolute path, e.g. `/home/me/.config/codewhale/credentials.json`.
  2. Build the path from an explicit base directory (`dirs::home_dir()` or a config root) joined with the relative part, then pass the joined absolute result.
  3. Check the config/env entry that supplies the path and add the missing leading `/`.
  4. If the path comes from another tool, print it (`path.is_absolute()`) and see where the relative form originates.

Example fix

// before
let creds = read_to_string("credentials/token")?;
// after
let path = std::path::Path::new(&home).join(".config/codewhale/credentials/token");
let creds = read_to_string(&path)?; // absolute
Defensive patterns

Strategy: validation

Validate before calling

if !path.is_absolute() {
    return Err(anyhow::anyhow!("credential path must be absolute: {path:?}"));
}

Type guard

fn is_absolute_credential_path(p: &Path) -> bool {
    p.is_absolute()
}

Try / catch

match read_to_string(&cred_path) {
    Err(e) if e.to_string().contains("must be absolute") => {
        eprintln!("{cred_path:?} is relative; build it from an absolute base dir");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling `read_to_string`/`read_codewhale_owned_to_string` (-> `open_secure_regular_file`) with a relative path such as `credentials/token` instead of `/home/me/.config/.../credentials/token`.

Common situations: Config files or environment variables holding a credential path written as a relative path; code constructing the path by string concatenation without a leading `/`; moving a working config between machines where the base directory differs.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

        );
    }
    String::from_utf8(bytes).map(Some).with_context(|| {
        format!(
            "Codewhale-owned credential file {} is not valid UTF-8",
            codewhale_config::quote_os_path(path)
        )
    })
}

#[cfg(unix)]
fn open_secure_regular_file(path: &Path, require_owner_only: bool) -> io::Result<File> {
    use std::ffi::CString;
    use std::os::fd::FromRawFd;
    use std::os::unix::ffi::OsStrExt;
    use std::path::Component;

    if !path.is_absolute() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "external credential path must be absolute",
        ));
    }

    let root = CString::new("/").expect("static root contains no NUL");
    // SAFETY: `root` is a valid C string and flags require no variadic mode.
    let root_fd = unsafe {
        libc::open(
            root.as_ptr(),
            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
        )
    };
    if root_fd < 0 {
        return Err(io::Error::last_os_error());
    }
    // SAFETY: `root_fd` is newly owned after the successful `open`.
    let mut current = unsafe { File::from_raw_fd(root_fd) };

View on GitHub (pinned to 73e0f67d83)