jdx/mise · error

refusing to replace non-file path: {}

Error message

refusing to replace non-file path: {}

What it means

write_file() builds the complete replacement in a temp file, then atomically persists it over the target. Before that, symlink_metadata on the destination must be absent or a regular file. Any other existing type (symlink, directory, fifo, socket) with replace not enabled is refused. With replace = true, directories are removed via remove_dir (non-empty directories fail with a wrapped error) and other non-files via remove_file.

Source

Thrown at src/system/managed_files.rs:1376

            parent.display()
        ),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            bail!("managed file parent does not exist: {}", parent.display())
        }
        Err(error) => return Err(error.into()),
    }
    // Prepare the complete replacement before mutating the destination. In
    // particular, a metadata permission error must leave the old path intact.
    let mut temporary = tempfile::NamedTempFile::new_in(parent)?;
    temporary.write_all(content)?;
    set_metadata(temporary.path(), owner, group, mode)?;
    temporary.as_file_mut().sync_all()?;
    match fs::symlink_metadata(path) {
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
        Err(error) => return Err(error.into()),
        Ok(metadata) if metadata.file_type().is_file() => {}
        Ok(_) if !replace => {
            bail!("refusing to replace non-file path: {}", path.display())
        }
        Ok(metadata) if metadata.file_type().is_dir() => {
            fs::remove_dir(path).wrap_err_with(|| {
                format!(
                    "refusing to replace non-empty directory with file: {}",
                    path.display()
                )
            })?
        }
        Ok(_) => fs::remove_file(path)?,
    }
    temporary
        .persist(path)
        .map_err(|error| error.error)
        .wrap_err_with(|| format!("failed to atomically replace {}", path.display()))?;
    fs::File::open(parent)?.sync_all()?;
    Ok(())
}

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Set replace = true on the entry so mise removes the non-file and puts the managed file in place
  2. Remove the symlink/directory yourself if you want to keep replace off as a safety stance
  3. If it is a directory, make sure it is empty - replacing a non-empty directory with a file fails by design

Example fix

# before: /etc/app/app.conf is currently a symlink -> error
[bootstrap.files]
"/etc/app/app.conf" = { content = "...", mode = "0644" }

# after
[bootstrap.files]
"/etc/app/app.conf" = { content = "...", mode = "0644", replace = true }
Defensive patterns

Strategy: validation

Validate before calling

match std::fs::symlink_metadata(path) {
    Ok(m) if !m.file_type().is_file() && !replace => {
        return Err(eyre::eyre!("destination is a non-file; set replace = true"));
    }
    _ => {}
}

Type guard

fn can_write_without_replace(path: &std::path::Path) -> bool {
    match std::fs::symlink_metadata(path) {
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
        Ok(m) => m.file_type().is_file(),
        _ => false,
    }
}

Try / catch

match write_file(path, content, owner, group, mode, replace) {
    Err(e) if e.to_string().contains("refusing to replace non-file path") => {
        // decide explicitly: set replace = true in config, or remove the symlink/dir manually
    }
    other => other?,
}

Prevention

When it happens

Trigger: The target path is currently a symlink (symlink_metadata sees the link itself, not its target) or a directory, and the [bootstrap.files] entry does not set replace = true.

Common situations: System path already managed by /etc/alternatives, a dotfiles manager, or another config tool via symlink; a directory sits where a file is declared.

Related errors


AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17). Data as JSON: /api/errors/df02bd570aca6da7. Report an issue: GitHub.