jdx/mise · warning · eyre::Report
refusing to follow symlink
Error message
refusing to follow symlink {} from an untrusted parent directory What it means
This error is thrown when resolving a path inside mise's managed-file tracking encounters a symlink whose parent directory is untrusted. A symlink is only followed if the parent directory is owned by root and not group/world-writable (mode 0o022 clear); otherwise mise bails out rather than risk following an attacker-controlled symlink (TOCTOU hardening).
Solutions
- Replace the symlink with a real directory (rm the symlink, mkdir the path)
- Ensure the parent directory of the symlink is owned by root and not group/world-writable (chown root, chmod o-w,g-w)
- Move the managed path to a location you control and mise trusts
- Check for tools or scripts that create symlinks in your mise data directories
Example fix
// before ls -la ~/.cache # cache -> /tmp/shared-cache (symlink in user-owned parent) // after rm ~/.cache && mkdir ~/.cache # real directory, error goes away
Defensive patterns
Strategy: validation
Validate before calling
import os, stat
def symlink_parent_trusted(path):
parent = os.path.dirname(os.path.abspath(path))
st = os.stat(parent)
return st.st_uid == 0 and (st.st_mode & 0o022) == 0 Type guard
def is_symlink(p):
return os.path.islink(p) Try / catch
match managed_dir.open(path) {
Err(e) if e.to_string().contains("untrusted parent directory") => {
// replace symlink with real dir, then retry
}
r => r,
} Prevention
- Do not place symlinks inside mise data/cache directories
- Keep parent directories root-owned and not group/world-writable
- Avoid running mise against paths under /tmp or shared writable dirs
- Audit dotfile managers that replace directories with symlinks
When it happens
Trigger: A path component walked by open_or_create_directory_tree_inner is a symlink (AT_SYMLINK_NOFOLLOW detects S_IFLNK) and its parent directory's st_uid is not 0 or its mode has group/world write bits set.
Common situations: Running mise against a shared or user-owned cache/scratch directory where a component like tmp or cache has been replaced by a symlink; attacker-planted symlinks in a world-writable directory like /tmp; containers running as non-root with symlinks in HOME.
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 resolve managed directory
- refusing to write dependency sidecar through symlink
- app Info.plist must be a regular file
- binary target is not an owned Caskroom symlink
AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17).
Data as JSON: /api/errors/8fd00797c8845fc8.
Report an issue: GitHub.
Appendix: source
Thrown at src/system/managed_files.rs:1680
_ => bail!("invalid managed directory path: {}", path.display()),
})
.collect::<Result<Vec<_>>>()?;
let flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW;
let mut directory = open(Path::new("/"), flags, Mode::empty())?;
let mut current = PathBuf::from("/");
for (index, name) in components.iter().enumerate() {
let component_path = current.join(name);
directory = match openat(&directory, name.as_os_str(), flags, Mode::empty()) {
Ok(directory) => directory,
Err(open_error) => {
let metadata = fstatat(&directory, name.as_os_str(), AtFlags::AT_SYMLINK_NOFOLLOW);
if metadata.is_ok_and(|metadata| {
SFlag::from_bits_truncate(metadata.st_mode).contains(SFlag::S_IFLNK)
}) {
let parent = fstat(&directory)?;
if parent.st_uid != 0 || parent.st_mode & 0o022 != 0 {
bail!(
"refusing to follow symlink {} from an untrusted parent directory",
component_path.display()
);
}
let target = nix::fcntl::readlinkat(&directory, name.as_os_str())?;
let mut resolved = if Path::new(&target).is_absolute() {
PathBuf::from(target)
} else {
current.join(target)
};
resolved.extend(components.iter().skip(index + 1));
let resolved = resolved.absolutize()?.to_path_buf();
if resolved == Path::new("/") {
bail!(
"refusing to resolve managed directory {} to the filesystem root",
path.display()
);
}View on GitHub (pinned to 533346cc37)