jdx/mise · error · eyre::Report

brew-cask: {APP_DIR_ENV} '{}' must not resolve to the filesy

Error message

brew-cask: {APP_DIR_ENV} '{}' must not resolve to the filesystem root

What it means

target_app_dir canonicalizes the MISE_BREW_CASK_OPT_APPDIR override (resolve_appdir follows symlinks in the longest existing prefix) and then requires at least one Normal path component. Every spelling of the filesystem root ('/', '//', '/.', a symlink to '/') collapses to '/' with no Normal components and is rejected: an appdir of root would make Path::starts_with(appdir) true for every path, i.e. disable the containment boundary entirely for privileged writes.

Source

Thrown at src/system/packages/brew/cask.rs:6059

    {
        bail!(
            "brew-cask: {APP_DIR_ENV} '{}' must not contain '..'",
            dir.display()
        );
    }
    // Resolve the override to a real absolute path: canonicalize its longest
    // existing prefix and re-append the components that do not exist yet. This
    // makes the appdir a symlink-free containment boundary — privileged cask
    // mutations then operate on resolved paths and cannot be redirected through
    // a symlinked component — and it collapses every spelling of the filesystem
    // root (`/`, `//`, `/.`, a symlink to `/`, ...) to `/` so they can all be
    // rejected together.
    let resolved = resolve_appdir(&dir);
    if !resolved
        .components()
        .any(|component| matches!(component, Component::Normal(_)))
    {
        bail!(
            "brew-cask: {APP_DIR_ENV} '{}' must not resolve to the filesystem root",
            dir.display()
        );
    }
    Ok(resolved)
}

/// Resolve `dir` by canonicalizing its longest existing ancestor and
/// re-appending the not-yet-existing tail. Symlinks in the existing portion are
/// followed, so the result is a real path the caller can safely use as a
/// containment boundary. Falls back to `dir` unchanged if nothing along the
/// path can be canonicalized (not expected for an absolute path, where `/`
/// always resolves).
fn resolve_appdir(dir: &Path) -> PathBuf {
    for ancestor in dir.ancestors() {
        if let Ok(real) = ancestor.canonicalize() {
            let tail = dir.strip_prefix(ancestor).unwrap_or(Path::new(""));
            return real.join(tail);

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Point the override at a real subdirectory: `export MISE_BREW_CASK_OPT_APPDIR=/opt/MyApps`
  2. Verify what the value resolves to: `realpath "$MISE_BREW_CASK_OPT_APPDIR"` must not be '/'
  3. Or unset the variable to use the default /Applications

Example fix

# before
export MISE_BREW_CASK_OPT_APPDIR=/
# after
export MISE_BREW_CASK_OPT_APPDIR=/Applications
Defensive patterns

Strategy: validation

Validate before calling

fn appdir_env_not_root(val: &str) -> bool {
    let resolved = std::path::Path::new(val)
        .canonicalize()
        .unwrap_or_else(|_| std::path::PathBuf::from(val));
    resolved.components().any(|c| matches!(c, std::path::Component::Normal(_)))
}

Try / catch

match target_app_dir() {
    Ok(dir) => dir,
    Err(e) if e.to_string().contains("filesystem root") => {
        std::env::remove_var("MISE_BREW_CASK_OPT_APPDIR");
        target_app_dir()
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Exporting MISE_BREW_CASK_OPT_APPDIR="/", "//", "/." or a symlink that resolves to the filesystem root.

Common situations: Scripts programmatically building the appdir from an empty or '/' base path; users experimenting with the override; symlinked dirs whose canonical target is root.

Related errors


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