jdx/mise · error
refusing to write dependency sidecar through symlink
Error message
refusing to write dependency sidecar through symlink {} What it means
When publishing dependency sidecar files, `publish_files` walks each target's parent directories up to the managed root and refuses to write if any ancestor is a symlink. This prevents a symlink planted inside the tool directory from redirecting writes outside the managed tree (symlink attack / accidental escape).
Solutions
- Replace the symlinked ancestor directory with a real directory (e.g. use bind mounts or move data and keep mise's default layout)
- Reconfigure mise's data/install directory to a real path instead of symlinking subdirectories
- Identify the offending symlink from the error message and remove it
- Regenerate/reinstall the affected tool so directories are created normally by mise
Example fix
# before: installs dir symlinked for dotfile management ln -s /mnt/data/mise/installs ~/.local/share/mise/installs # after: real directory, data moved mv ~/.local/share/mise/installs /mnt/data/mise/installs # configure root path instead of symlinking
Defensive patterns
Strategy: validation
Validate before calling
use std::path::Path;
fn ancestors_have_no_symlinks(target: &Path, root: &Path) -> bool {
target.parent().unwrap_or(root)
.ancestors().take_while(|p| p.starts_with(root))
.all(|p| !p.is_symlink())
} Try / catch
match result {
Err(e) if e.to_string().contains("refusing to write dependency sidecar through symlink") => {
// inspect the reported ancestor path, replace symlink with a real directory, retry
}
other => other?,
} Prevention
- Don't symlink subdirectories of mise's data/installs tree; configure real paths instead
- Use bind mounts rather than symlinks when relocating tool storage
- Audit tool install directories for unexpected symlinks on shared machines
- Run `mise doctor` after restructuring mise's directory layout
When it happens
Trigger: Publishing sidecar files when a directory between the root and the sidecar location (e.g. `~/.local/share/mise/installs/node`) is a symlink — e.g. the user symlinked their installs directory to another disk, or a malicious/planted symlink exists in the tree.
Common situations: Users symlinking parts of `~/.local/share/mise` (dotfile management, moving installs to another volume); tools that symlink their installation directories; compromised or shared multi-user machines.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- a parent directory is now a symlink; left untouched
- refusing to follow symlink
- app Info.plist must be a regular file
- binary target is not an owned Caskroom symlink
- brew-cask: refusing elevated operation because target…
AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17).
Data as JSON: /api/errors/d1d6e360abc9ff17.
Report an issue: GitHub.
Appendix: source
Thrown at src/lockfile/graph.rs:415
}
if dir.join("uv.lock").is_file() || dir.join("aube-lock.yaml").is_file() {
self.remove.push(dir);
}
}
}
}
pub(super) fn has_changes(&self) -> bool {
!self.files.is_empty() || !self.remove.is_empty()
}
pub(super) fn publish_files(&self) -> Result<()> {
use std::io::Write;
for (target, text) in &self.files {
let parent = target
.parent()
.ok_or_else(|| eyre!("invalid sidecar file path"))?;
for ancestor in parent.ancestors().take_while(|p| p.starts_with(&self.root)) {
if ancestor.is_symlink() {
bail!(
"refusing to write dependency sidecar through symlink {}",
ancestor.display()
);
}
}
std::fs::create_dir_all(parent)?;
let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
tmp.write_all(text.as_bytes())?;
tmp.as_file().sync_all()?;
tmp.persist(target)?;
}
Ok(())
}
pub(super) fn prune(&self) -> Result<()> {
for dir in &self.remove {
if dir.exists() {
std::fs::remove_dir_all(dir)?;
}View on GitHub (pinned to 533346cc37)