Hmbown/CodeWhale · error · io::Error
Codewhale-owned credential file must be singly linked
Error message
Codewhale-owned credential file must be singly linked
What it means
For Codewhale-owned credentials on Windows, the library queries GetFileInformationByHandle and requires nNumberOfLinks == 1, i.e. the file must have no other hard links. It throws PermissionDenied otherwise, because a second hard link lets another local process hold an alternate name for the secret that survives permission checks on the primary path.
Solutions
- Delete the extra hard links (`fsutil hardlink list <file>` to find them), leaving exactly one name.
- Recreate the credential as a fresh single file: write a new file with restrictive ACLs and update the configuration.
- Exclude the credential directory from deduplicating backup tools.
- Never share the credential by hard link; grant access via ACLs instead.
Example fix
// cmd, before mklink /H C:\tools\token.json C:\Users\me\.codewhale\token.json // after del C:\tools\token.json fsutil hardlink list C:\Users\me\.codewhale\token.json // should show a single link
Defensive patterns
Strategy: validation
Validate before calling
// cmd pre-check: fsutil hardlink list <file> — must print exactly one path
Try / catch
match read_codewhale_owned_to_string(&path) {
Ok(creds) => use(creds),
Err(e) if e.to_string().contains("singly linked") => {
eprintln!("remove extra hard links: fsutil hardlink list {}", path.display());
}
Err(e) => return Err(e),
} Prevention
- Never create hard links (mklink /H) to credential files.
- Exclude credential directories from deduplicating backup software.
- If sharing a secret with another app is needed, use ACL grants, not links.
- Recreate the credential file fresh if link count is ever unclear.
When it happens
Trigger: read_codewhale_owned_to_string opens a Windows file whose link count is greater than 1, typically because `mklink /H` or `fsutil hardlink create` made an alias, or a copy tool created hard links.
Common situations: Backup/snapshot tools (dedupe, Storage Spaces, third-party backup) hard-link the credential file; a user created a hard link to share the token with another app; a container bind of the same underlying file shows extra links.
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
- external credential path must name a non-reparse regular…
- external credential path was redirected while opening
- private sidecar file
- xAI OAuth file must not have multiple filesystem links
- Automation lock must not be a reparse point
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/1dea00f9cab01be9.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/external_credentials.rs:331
let expected = normalize_windows_path_for_comparison(path)?;
if actual != expected {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"external credential path was redirected while opening",
));
}
if require_owner_only {
use windows_sys::Win32::Storage::FileSystem::{
BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle,
};
let mut information = BY_HANDLE_FILE_INFORMATION::default();
// SAFETY: the opened credential handle and output pointer remain valid
// for the duration of the call.
if unsafe { GetFileInformationByHandle(handle, &mut information) } == 0 {
return Err(io::Error::last_os_error());
}
if information.nNumberOfLinks != 1 {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"Codewhale-owned credential file must be singly linked",
));
}
verify_windows_owner_only_handle(handle)?;
}
Ok(file)
}
/// Normalize a Windows path without replacement characters. Unpaired UTF-16
/// is rejected so two distinct paths can never compare equal after a lossy
/// conversion. This is intentionally stricter than filesystem display.
#[cfg(windows)]
fn normalize_windows_path_for_comparison(path: &Path) -> io::Result<String> {
let text = path.to_str().ok_or_else(|| {
io::Error::new(
io::ErrorKind::PermissionDenied,
"credential path contains invalid Unicode and cannot be compared safely",View on GitHub (pinned to 73e0f67d83)