{"record":{"id":"ef53c9d026388e4c","repo":"jdx/mise","slug":"directory-changed-while-restoring-permissions","errorCode":null,"errorMessage":"directory changed while restoring permissions: {}","messagePattern":"directory changed while restoring permissions: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/history/replay.rs","lineNumber":1602,"sourceCode":"    // a regular file is replaced in place: the atomic write keeps its mode\n    file::write_atomic(path, &bytes)?;\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::PermissionsExt;\n        std::fs::set_permissions(path, std::fs::Permissions::from_mode(bits))?;\n    }\n    Ok(())\n}\n\n/// Puts back the recorded mode of the directories above a restored file\n/// (a `0700` directory recreated by `create_dir_all` would be `0755`).\n#[cfg(unix)]\nfn restore_dir_modes(_step: &Step, created: &[(PathBuf, u32)]) -> Result<()> {\n    use std::os::unix::fs::PermissionsExt;\n    for (dir, bits) in created {\n        let metadata = std::fs::symlink_metadata(dir)?;\n        if !metadata.is_dir() || metadata.file_type().is_symlink() {\n            bail!(\n                \"directory changed while restoring permissions: {}\",\n                display_path(dir)\n            );\n        }\n        std::fs::set_permissions(dir, std::fs::Permissions::from_mode(*bits))?;\n    }\n    Ok(())\n}\n\n#[cfg(not(unix))]\nfn restore_dir_modes(step: &Step, _created: &[(PathBuf, u32)]) -> Result<()> {\n    if step.bits.is_some() || !step.dir_bits.is_empty() {\n        debug!(\n            \"history: {}: recorded permission bits are not restored on this platform\",\n            display_path(&step.path)\n        );\n    }\n    Ok(())","sourceCodeStart":1584,"sourceCodeEnd":1620,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/history/replay.rs#L1584-L1620","documentation":"After restoring file objects during replay, restore_dir_modes reapplies saved permission bits to directories it created. If a recorded path is no longer a real directory (it was replaced by a symlink or a file, or removed), mise refuses to chmod it because doing so could follow a symlink and alter the wrong target.","triggerScenarios":"During write_object recovery: a path recorded in `created` was replaced by a symlink or regular file between checkpoint and restore; another process swapped the directory; a partial rollback replaced the directory with a file.","commonSituations":"Tool runs that symlink directories over mise-managed paths; concurrent mise or editor processes mutating the tree during recovery; restoring onto a checkout where directories became files.","solutions":["Inspect the reported path: if it is now a symlink or file and that is intended, remove it and re-run the operation so mise recreates the directory","Re-create the directory before retrying recovery","Ensure no other process is concurrently modifying the project tree during history operations"],"exampleFix":"// path replaced by symlink\nln -sfn elsewhere dir   # breaks restore\n// after\nrm dir && mkdir dir      # real directory again, then retry","handlingStrategy":"validation","validationCode":"for dir in created_dirs {\n    let md = std::fs::symlink_metadata(dir)?;\n    if !md.is_dir() || md.file_type().is_symlink() {\n        eprintln!(\"{} is no longer a real directory\", dir.display());\n    }\n}","typeGuard":"fn is_real_dir(p: &Path) -> bool {\n    std::fs::symlink_metadata(p).map(|m| m.is_dir() && !m.file_type().is_symlink()).unwrap_or(false)\n}","tryCatchPattern":null,"preventionTips":["Do not symlink over directories managed by mise history","Avoid running other tools that mutate the tree during a history operation","Re-create replaced directories before retrying recovery","Serialize tree-mutating jobs"],"tags":["unix","permissions","filesystem-race","recovery"],"backgroundTag":"internal-invariant-violation","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}