{"record":{"id":"5f49de3979421b8c","repo":"jdx/mise","slug":"brew-cask-app-dir-env-must-not-resolve-to","errorCode":null,"errorMessage":"brew-cask: {APP_DIR_ENV} '{}' must not resolve to the filesystem root","messagePattern":"brew-cask: (.+?) '(.+?)' must not resolve to the filesystem root","errorType":"validation","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":6059,"sourceCode":"    {\n        bail!(\n            \"brew-cask: {APP_DIR_ENV} '{}' must not contain '..'\",\n            dir.display()\n        );\n    }\n    // Resolve the override to a real absolute path: canonicalize its longest\n    // existing prefix and re-append the components that do not exist yet. This\n    // makes the appdir a symlink-free containment boundary — privileged cask\n    // mutations then operate on resolved paths and cannot be redirected through\n    // a symlinked component — and it collapses every spelling of the filesystem\n    // root (`/`, `//`, `/.`, a symlink to `/`, ...) to `/` so they can all be\n    // rejected together.\n    let resolved = resolve_appdir(&dir);\n    if !resolved\n        .components()\n        .any(|component| matches!(component, Component::Normal(_)))\n    {\n        bail!(\n            \"brew-cask: {APP_DIR_ENV} '{}' must not resolve to the filesystem root\",\n            dir.display()\n        );\n    }\n    Ok(resolved)\n}\n\n/// Resolve `dir` by canonicalizing its longest existing ancestor and\n/// re-appending the not-yet-existing tail. Symlinks in the existing portion are\n/// followed, so the result is a real path the caller can safely use as a\n/// containment boundary. Falls back to `dir` unchanged if nothing along the\n/// path can be canonicalized (not expected for an absolute path, where `/`\n/// always resolves).\nfn resolve_appdir(dir: &Path) -> PathBuf {\n    for ancestor in dir.ancestors() {\n        if let Ok(real) = ancestor.canonicalize() {\n            let tail = dir.strip_prefix(ancestor).unwrap_or(Path::new(\"\"));\n            return real.join(tail);","sourceCodeStart":6041,"sourceCodeEnd":6077,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/packages/brew/cask.rs#L6041-L6077","documentation":"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.","triggerScenarios":"Exporting MISE_BREW_CASK_OPT_APPDIR=\"/\", \"//\", \"/.\" or a symlink that resolves to the filesystem root.","commonSituations":"Scripts programmatically building the appdir from an empty or '/' base path; users experimenting with the override; symlinked dirs whose canonical target is root.","solutions":["Point the override at a real subdirectory: `export MISE_BREW_CASK_OPT_APPDIR=/opt/MyApps`","Verify what the value resolves to: `realpath \"$MISE_BREW_CASK_OPT_APPDIR\"` must not be '/'","Or unset the variable to use the default /Applications"],"exampleFix":"# before\nexport MISE_BREW_CASK_OPT_APPDIR=/\n# after\nexport MISE_BREW_CASK_OPT_APPDIR=/Applications","handlingStrategy":"validation","validationCode":"fn appdir_env_not_root(val: &str) -> bool {\n    let resolved = std::path::Path::new(val)\n        .canonicalize()\n        .unwrap_or_else(|_| std::path::PathBuf::from(val));\n    resolved.components().any(|c| matches!(c, std::path::Component::Normal(_)))\n}","typeGuard":null,"tryCatchPattern":"match target_app_dir() {\n    Ok(dir) => dir,\n    Err(e) if e.to_string().contains(\"filesystem root\") => {\n        std::env::remove_var(\"MISE_BREW_CASK_OPT_APPDIR\");\n        target_app_dir()\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Point MISE_BREW_CASK_OPT_APPDIR at a real subdirectory, never '/', '//', or a root symlink","Verify with realpath \"$MISE_BREW_CASK_OPT_APPDIR\" before launching mise"],"tags":["brew-cask","env-var","appdir","path-containment","hardening"],"backgroundTag":"invalid-env-var-value","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}