jdx/mise · error
directory changed while restoring permissions: {}
Error message
directory changed while restoring permissions: {} What it means
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.
Source
Thrown at src/system/history/replay.rs:1602
// a regular file is replaced in place: the atomic write keeps its mode
file::write_atomic(path, &bytes)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(bits))?;
}
Ok(())
}
/// Puts back the recorded mode of the directories above a restored file
/// (a `0700` directory recreated by `create_dir_all` would be `0755`).
#[cfg(unix)]
fn restore_dir_modes(_step: &Step, created: &[(PathBuf, u32)]) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
for (dir, bits) in created {
let metadata = std::fs::symlink_metadata(dir)?;
if !metadata.is_dir() || metadata.file_type().is_symlink() {
bail!(
"directory changed while restoring permissions: {}",
display_path(dir)
);
}
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(*bits))?;
}
Ok(())
}
#[cfg(not(unix))]
fn restore_dir_modes(step: &Step, _created: &[(PathBuf, u32)]) -> Result<()> {
if step.bits.is_some() || !step.dir_bits.is_empty() {
debug!(
"history: {}: recorded permission bits are not restored on this platform",
display_path(&step.path)
);
}
Ok(())View on GitHub (pinned to afd2eddd3a)
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
Example fix
// path replaced by symlink ln -sfn elsewhere dir # breaks restore // after rm dir && mkdir dir # real directory again, then retry
Defensive patterns
Strategy: validation
Validate before calling
for dir in created_dirs {
let md = std::fs::symlink_metadata(dir)?;
if !md.is_dir() || md.file_type().is_symlink() {
eprintln!("{} is no longer a real directory", dir.display());
}
} Type guard
fn is_real_dir(p: &Path) -> bool {
std::fs::symlink_metadata(p).map(|m| m.is_dir() && !m.file_type().is_symlink()).unwrap_or(false)
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- mode must be between 0000 and 7777
- brew-cask: temporary artifact directory is not private
- `docker load` failed ({}): {}. Ensure the docker daemon is r
- only available on unix
- brew-cask: temporary artifact directory is not private
AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09).
Data as JSON: /api/errors/ef53c9d026388e4c.
Report an issue: GitHub.