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
- Set replace = true on the entry so mise removes the non-file and puts the managed file in place
- Remove the symlink/directory yourself if you want to keep replace off as a safety stance
- 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
- Audit existing symlinks/dirs at target paths before declaring managed files
- Set replace = true only where takeover is intended; leave it off as a safety default
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
- refusing to replace non-directory path: {}
- too many symlinks while resolving atomic write target: {}
- refusing to remove non-link: {}
- refusing to manage the filesystem root
- managed file parent is not a directory: {}
AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17).
Data as JSON: /api/errors/df02bd570aca6da7.
Report an issue: GitHub.