{"record":{"id":"aa6fd905b6b5b568","repo":"zeroclaw-labs/zeroclaw","slug":"key-file-path-is-a-reparse-point-refusing-to-rea","errorCode":null,"errorMessage":"Key file path is a reparse point — refusing to read","messagePattern":"Key file path is a reparse point — refusing to read","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-config/src/secrets.rs","lineNumber":538,"sourceCode":"#[cfg(windows)]\nfn open_no_follow(key_path: &Path) -> std::io::Result<std::fs::File> {\n    use std::os::windows::fs::{MetadataExt, OpenOptionsExt};\n    // FILE_FLAG_OPEN_REPARSE_POINT (0x0020_0000): open the reparse point itself\n    // instead of following it.  FILE_FLAG_BACKUP_SEMANTICS (0x0200_0000) lets the\n    // call also work if the entry is a directory reparse point.\n    const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;\n    const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;\n\n    let file = std::fs::OpenOptions::new()\n        .read(true)\n        .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS)\n        .open(key_path)?;\n\n    // Inspect the SAME handle we will read from.  If it carries the\n    // reparse-point attribute, refuse.\n    let attrs = file.metadata()?.file_attributes();\n    if attrs & FILE_ATTRIBUTE_REPARSE_POINT != 0 {\n        return Err(std::io::Error::new(\n            std::io::ErrorKind::InvalidInput,\n            \"Key file path is a reparse point — refusing to read\",\n        ));\n    }\n    Ok(file)\n}\n\n#[cfg(not(any(unix, windows)))]\nfn open_no_follow(_key_path: &Path) -> std::io::Result<std::fs::File> {\n    compile_error!(\n        \"no-follow key reads require platform symlink/reparse-point semantics \\\n         (O_NOFOLLOW on Unix, reparse-point attribute on Windows); unsupported target\"\n    );\n}\n\n/// Read the key file from a no-follow / reparse-point-verified handle.\n///\n/// Opening with `open_no_follow` binds the \"not a symlink\" check to the same","sourceCodeStart":520,"sourceCodeEnd":556,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-config/src/secrets.rs#L520-L556","documentation":"Windows counterpart of the symlink guard: after opening the key file, the same handle's metadata is inspected and FILE_ATTRIBUTE_REPARSE_POINT (which covers NTFS symlinks, junctions, and OneDrive placeholders) triggers a refusal. The read never goes through a reparse indirection, blocking swap attacks via reparse points.","triggerScenarios":"provisioning_state or read_key_file_no_follow opens a key path on Windows that is an NTFS symlink, a directory junction target, a mklink'd file, or a cloud placeholder carrying the reparse attribute (OneDrive, Dev Drive dedupe).","commonSituations":"Config directories inside OneDrive-synced folders (placeholders carry the attribute even when hydrated), developers using junctions to share keys between checkouts, mklink experiments, CI on Windows runners with linked secret directories.","solutions":["Move the key file to a plain NTFS location outside any reparse-pointed or cloud-synced directory and update config.","Exclude the keys directory from OneDrive/cloud sync so placeholders are not created.","Copy the key to a regular file at the expected path instead of linking (copy, mklink without reparse is not possible; a hardlink is fine only if the tool accepts it, but a plain copy is the safe answer).","Verify with 'fsutil reparsepoint query <path>' or 'dir /a' (shows <SYMLINK>/<JUNCTION>) before reporting a bug."],"exampleFix":":: before\nC:\\Users\\me\\OneDrive\\zeroclaw\\keys\\agent.key   (cloud placeholder -> refused)\n\n:: after\nmkdir C:\\zeroclaw-keys\ncopy C:\\Users\\me\\OneDrive\\zeroclaw\\keys\\agent.key C:\\zeroclaw-keys\\agent.key\n:: point config at C:\\zeroclaw-keys\\agent.key","handlingStrategy":"validation","validationCode":"// Windows: check the reparse attribute before the runtime opens it\n#[cfg(windows)]\nfn key_file_is_plain(path: &std::path::Path) -> bool {\n    const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;\n    std::fs::metadata(path)\n        .map(|m| m.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT == 0)\n        .unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"match secrets::read_key_file_no_follow(path) {\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidInput && e.to_string().contains(\"reparse point\") => {\n        Err(anyhow!(\"key {path} is a reparse point; copy it to a plain directory and update config\"))\n    }\n    other => other,\n}","preventionTips":["Keep key directories outside OneDrive/cloud-synced folders on Windows.","Prefer junction-free plain paths like C:\\zeroclaw-keys over links between checkouts.","Verify with 'dir /a' or 'fsutil reparsepoint query' whenever a key path was set up with links."],"tags":["secrets","key-file","reparse-point","security","windows"],"backgroundTag":"key-file-symlink-refused","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}