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

  1. 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
  2. Re-create the directory before retrying recovery
  3. 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

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


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/ef53c9d026388e4c. Report an issue: GitHub.