{"record":{"id":"7388ca343a900bc3","repo":"jdx/mise","slug":"brew-cask-app-dir-env-must-not-resolve-to-7388ca","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":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask/paths.rs","lineNumber":194,"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).\npub(super) fn 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":176,"sourceCodeEnd":212,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/packages/brew/cask/paths.rs#L176-L212","documentation":"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).","triggerScenarios":"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).","commonSituations":"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 '/'.","solutions":["Choose a real dedicated directory, e.g. export MISE_BREW_CASK_OPT_APPDIR=/opt/homebrew-apps, and create it with mkdir -p first.","If you wanted the default behavior, unset the variable (or set it empty) so target_app_dir falls back to /Applications.","Check whether a symlink component resolves to '/' (ls -l or realpath on the dir); replace the value with the actual target directory.","Never point the appdir at the filesystem root — the check exists to keep privileged cask mutations bounded."],"exampleFix":"// before\nexport MISE_BREW_CASK_OPT_APPDIR=/\n// after\nmkdir -p /opt/apps && export MISE_BREW_CASK_OPT_APPDIR=/opt/apps","handlingStrategy":"validation","validationCode":"function isValidAppdir(v) {\n  const p = v ? path.resolve(v) : null;\n  return !!p && path.isAbsolute(v) && !v.split('/').includes('..') && p !== '/';\n}","typeGuard":"function isSafeAppdir(v) {\n  return typeof v === \"string\" && v.length > 0 && path.isAbsolute(v) &&\n    !v.includes('..') && path.resolve(v) !== path.parse(v).root;\n}","tryCatchPattern":null,"preventionTips":["Never set MISE_BREW_CASK_OPT_APPDIR to '/', '//', '/.', or a path whose symlinks resolve to '/'.","Run realpath on the intended directory and assert it is not the root before exporting it.","Create a dedicated subdirectory (mkdir -p /opt/apps) rather than trying to scope to root.","Check symlink components with ls -l / realpath; a link to '/' makes the value invalid."],"tags":["brew-cask","env-var","path-validation"],"backgroundTag":"invalid-env-var-value","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}