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
- Fix ownership and mode: `chown $(id -u) <path> && chmod 600 <path>`.
- 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).
- 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.
- 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
- Create credential files with mode 0600 (set umask 077 before writing).
- Write secrets atomically: temp file in same dir with 0600, then rename.
- Exclude credential directories from hard-linking dedupe/backup tools.
- After copying or provisioning, always chown to the running user and chmod 600.
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
- PermissionDenied
- Automation lock must not have hard links
- Codewhale credentials directory must be owned by the…
- Codewhale-owned credential file must be singly linked
- Codewhale-owned xAI OAuth storage owner is not the current…
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)