Hmbown/CodeWhale · error · std::io::Error

InvalidInput

InvalidInput

Error message

external credential path must be absolute

What it means

open_secure_regular_file (Unix) requires external credential paths to be absolute before it walks from / with openat, refusing anything relative as the first gate of the no-follow traversal that opens the exact granted file. Relative paths fail immediately with InvalidInput — no cwd-based resolution is attempted, which is deliberate: a consented credential path must be unambiguous.

Solutions

  1. Use a fully expanded absolute path: export PROVIDER_CREDENTIALS=/home/user/.config/provider/token.json
  2. Expand ~ at assignment time in the shell: export PROVIDER_CREDENTIALS="$HOME/.config/provider/token.json"
  3. In code, absolutize against a config root or canonicalize before passing the path into the credential reader

Example fix

# before
export PROVIDER_CREDENTIALS="~/.config/provider/token.json"   # stays literal, relative

# after
export PROVIDER_CREDENTIALS="$HOME/.config/provider/token.json"   # expanded, absolute
Defensive patterns

Strategy: validation

Validate before calling

let path = Path::new(&value);
if !path.is_absolute() {
    return Err(format!("credential path must be absolute, got: {value:?}"));
}

Type guard

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

Prevention

When it happens

Trigger: An external-credential grant/config holding a relative value such as "./token" or "keys/api.json", or a tilde path like "~/.config/provider/token" — the OS does not expand ~ inside environment variables, so it stays relative and is rejected here.

Common situations: Env-based config written in .env files or scripts where the author assumed ~ expansion; running from a different working directory than when the relative path happened to work; values copy-pasted from docs showing shell-style paths.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/1e690c464e8304dc. Report an issue: GitHub.

Appendix: source

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

        );
    }
    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 0c42157ee5)