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

  1. Replace the symlinked ancestor directory with a real directory (e.g. use bind mounts or move data and keep mise's default layout)
  2. Reconfigure mise's data/install directory to a real path instead of symlinking subdirectories
  3. Identify the offending symlink from the error message and remove it
  4. 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

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


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)