{"record":{"id":"877ba700e798ebc9","repo":"Hmbown/CodeWhale","slug":"codewhale-owned-credential-file-must-be-singly-linked-owned","errorCode":null,"errorMessage":"Codewhale-owned credential file must be singly linked, owned by this user, and mode 0600 or stricter","messagePattern":"Codewhale-owned credential file must be singly linked, owned by this user, and mode 0600 or stricter","errorType":"validation","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/external_credentials.rs","lineNumber":236,"sourceCode":"            io::ErrorKind::InvalidInput,\n            \"external credential path must name a file\",\n        ));\n    }\n    let metadata = current.metadata()?;\n    if !metadata.file_type().is_file() {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidInput,\n            \"external credential path must name a regular file\",\n        ));\n    }\n    if require_owner_only {\n        use std::os::unix::fs::MetadataExt as _;\n        // SAFETY: geteuid(2) dereferences no pointers.\n        if metadata.uid() != unsafe { libc::geteuid() }\n            || metadata.mode() & 0o077 != 0\n            || metadata.nlink() != 1\n        {\n            return Err(io::Error::new(\n                io::ErrorKind::PermissionDenied,\n                \"Codewhale-owned credential file must be singly linked, owned by this user, and mode 0600 or stricter\",\n            ));\n        }\n    }\n    Ok(current)\n}\n\n#[cfg(windows)]\nfn open_secure_regular_file(path: &Path, require_owner_only: bool) -> io::Result<File> {\n    use std::ffi::OsString;\n    use std::os::windows::ffi::OsStringExt;\n    use std::os::windows::fs::{MetadataExt, OpenOptionsExt};\n    use std::os::windows::io::AsRawHandle;\n    use std::path::Component;\n    use windows_sys::Win32::Storage::FileSystem::{\n        FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_OPEN_REPARSE_POINT, FILE_NAME_OPENED,\n        GetFinalPathNameByHandleW, VOLUME_NAME_DOS,","sourceCodeStart":218,"sourceCodeEnd":254,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/external_credentials.rs#L218-L254","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// shell, before\nls -l ~/.codewhale/credentials  # -rw-r--r-- 2 root root\n// after\nsudo chown $(id -u) ~/.codewhale/credentials/token.json\nchmod 600 ~/.codewhale/credentials/token.json\nfind ~ -inum $(stat -c %i ~/.codewhale/credentials/token.json)  # find and remove other links","handlingStrategy":"validation","validationCode":"use std::os::unix::fs::MetadataExt;\nfn ensure_owner_only_unix(path: &Path) -> std::io::Result<()> {\n    let md = std::fs::metadata(path)?;\n    if md.uid() != unsafe { libc::geteuid() } || md.mode() & 0o077 != 0 || md.nlink() != 1 {\n        return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, \"credential must be 0600, singly linked, user-owned\"));\n    }\n    Ok(())\n}","typeGuard":"fn is_unix_owner_only(path: &Path) -> bool {\n    use std::os::unix::fs::MetadataExt;\n    std::fs::metadata(path).map(|md| {\n        md.mode() & 0o077 == 0 && md.nlink() == 1\n    }).unwrap_or(false)\n}","tryCatchPattern":"match read_codewhale_owned_to_string(&path) {\n    Ok(creds) => use(creds),\n    Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {\n        eprintln!(\"fix with: chown $(id -u) {p} && chmod 600 {p}; check nlink with ls -li\", p = path.display());\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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."],"tags":["security","permissions","filesystem","credentials","unix"],"backgroundTag":"permission-denied","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}