Hmbown/CodeWhale · error · io::Error
external credential path must name a file
Error message
external credential path must name a file
What it means
After walking every component with `openat`, `open_secure_regular_file` checks that the final component actually opened a leaf file (and subsequently that the metadata is a regular file). A path that ends in a directory or vanishes into a non-leaf (e.g. trailing slash semantics) is rejected with this `InvalidInput` error — the credential API only reads files, not directories.
Solutions
- Point the path at the credential file itself, not its directory: e.g. `/home/me/.config/codewhale/credentials.json`.
- Remove any trailing `/` from the configured path.
- Verify the credential file exists: `ls -la <path>` — if only the directory is present, complete the credential setup/login step that creates the file.
- Check whether the tool that should have written the credential file ran and had permission to create it.
Example fix
// before
let creds = read_to_string("/home/me/.config/codewhale/credentials/")?; // directory
// after
let creds = read_to_string("/home/me/.config/codewhale/credentials.json")?; Defensive patterns
Strategy: validation
Validate before calling
let md = std::fs::metadata(&path)?; // pre-check
if !md.is_file() {
return Err(anyhow::anyhow!("credential path must be a file, not a directory: {path:?}"));
} Type guard
fn names_regular_file(p: &Path) -> bool {
std::fs::metadata(p).map(|m| m.is_file()).unwrap_or(false)
} Try / catch
match read_to_string(&cred_path) {
Err(e) if e.to_string().contains("must name a file") => {
eprintln!("{cred_path:?} points at a directory; set the full path to the credential file.");
}
other => other?,
} Prevention
- Configure the full file path of credentials, never the containing directory.
- Strip trailing slashes from copied paths before storing them.
- Verify the credential file exists after login/setup flows complete.
- Check file-vs-directory in your config loader and fail with a precise message.
When it happens
Trigger: `read_to_string`/`read_codewhale_owned_to_string` called with a path that names a directory (e.g. `/home/me/.config/codewhale/` or a credentials directory rather than a file).
Common situations: Config value points at a directory (`~/.ssh`, `~/.config/codewhale/credentials/`) instead of the file inside it; trailing slash in a copied path; the expected credential file was never created so only the parent directory exists.
Related errors
- external credential path must be absolute
- atomically replacing xAI OAuth credentials
- Codewhale-owned credential file must be singly linked
- Codewhale-owned credential file must be singly linked…
- credential path contains invalid Unicode and cannot be…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/6c9f8eed2a923a78.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/external_credentials.rs:217
let flags = if leaf {
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK
} else {
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_DIRECTORY
};
use std::os::fd::AsRawFd;
// SAFETY: the directory fd and component C string are valid for this
// call and flags require no variadic mode.
let fd = unsafe { libc::openat(current.as_raw_fd(), component.as_ptr(), flags) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: `fd` is newly owned after the successful `openat`.
current = unsafe { File::from_raw_fd(fd) };
opened_leaf = leaf;
}
if !opened_leaf {
return Err(io::Error::new(
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
{View on GitHub (pinned to 73e0f67d83)