jdx/mise · error

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

Error message

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

What it means

The MISE_BREW_CASK_OPT_APPDIR override must not resolve to the filesystem root ('/'). The value is canonicalized (via resolve_appdir, which resolves symlinks in the existing prefix) and every spelling of root — '/', '//', '/.', or a symlink pointing at '/' — is collapsed and rejected, because a root appdir would let cask apps be linked anywhere on the filesystem (src/system/packages/brew/cask/paths.rs:194).

Solutions

  1. Choose a real dedicated directory, e.g. export MISE_BREW_CASK_OPT_APPDIR=/opt/homebrew-apps, and create it with mkdir -p first.
  2. If you wanted the default behavior, unset the variable (or set it empty) so target_app_dir falls back to /Applications.
  3. Check whether a symlink component resolves to '/' (ls -l or realpath on the dir); replace the value with the actual target directory.
  4. Never point the appdir at the filesystem root — the check exists to keep privileged cask mutations bounded.

Example fix

// before
export MISE_BREW_CASK_OPT_APPDIR=/
// after
mkdir -p /opt/apps && export MISE_BREW_CASK_OPT_APPDIR=/opt/apps
Defensive patterns

Strategy: validation

Validate before calling

function isValidAppdir(v) {
  const p = v ? path.resolve(v) : null;
  return !!p && path.isAbsolute(v) && !v.split('/').includes('..') && p !== '/';
}

Type guard

function isSafeAppdir(v) {
  return typeof v === "string" && v.length > 0 && path.isAbsolute(v) &&
    !v.includes('..') && path.resolve(v) !== path.parse(v).root;
}

Prevention

When it happens

Trigger: Setting MISE_BREW_CASK_OPT_APPDIR to '/', '//', '/.', '/Applications/..', or any path containing a symlink that resolves to '/', then running a cask install or validation that calls target_app_dir (via target_path, cask_appdir, app_target_path, or allowed_appdir_roots).

Common situations: Users attempting to disable appdir scoping by pointing it at root; shell logic that computes the dir by stripping components until nothing remains; misconfigured containers/chroots where the intended app dir is literally '/'.

Understand the failure class

Background: "is not a valid" / "Invalid ... value" environment variable errors: how libraries validate env vars and what to do when they reject yours — this error's family across 48 libraries.

Related errors


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/7388ca343a900bc3. Report an issue: GitHub.

Appendix: source

Thrown at src/system/packages/brew/cask/paths.rs:194

    {
        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).
pub(super) 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 533346cc37)