{"record":{"id":"69fd493e898b1bf4","repo":"jdx/mise","slug":"brew-cask-refusing-generic-artifact-copy-outside","errorCode":null,"errorMessage":"brew-cask: refusing generic artifact copy outside Homebrew prefix: {}","messagePattern":"brew-cask: refusing generic artifact copy outside Homebrew prefix: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":2063,"sourceCode":"    )?;\n    if bound.st_dev != linked.st_dev || bound.st_ino != linked.st_ino {\n        bail!(\"brew-cask: temporary artifact directory was replaced\");\n    }\n    nix::unistd::unlinkat(\n        &parent.fd,\n        staging_name,\n        nix::unistd::UnlinkatFlags::RemoveDir,\n    )?;\n    Ok(())\n}\n\nfn validate_generic_copy_target(target: &Path) -> Result<()> {\n    let prefix = prefix::prefix();\n    if !target.starts_with(&prefix)\n        || target.strip_prefix(&prefix)?.components().next().is_none()\n        || !path_starts_with_resolved_root(target, &prefix)\n    {\n        bail!(\n            \"brew-cask: refusing generic artifact copy outside Homebrew prefix: {}\",\n            target.display()\n        );\n    }\n    Ok(())\n}\n\n#[cfg(unix)]\nstruct TrustedOperationParent {\n    fd: std::os::fd::OwnedFd,\n}\n\n#[cfg(unix)]\nimpl TrustedOperationParent {\n    fn path(&self) -> Result<PathBuf> {\n        #[cfg(target_os = \"linux\")]\n        return Ok(\n            Path::new(\"/proc/self/fd\").join(std::os::fd::AsRawFd::as_raw_fd(&self.fd).to_string())","sourceCodeStart":2045,"sourceCodeEnd":2081,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/packages/brew/cask.rs#L2045-L2081","documentation":"Generic artifacts (plain files a cask copies into place) must be installed strictly inside the Homebrew prefix. validate_generic_copy_target rejects a target that is not under $HOMEBREW_PREFIX, that is the prefix itself (no component beneath it), or whose symlink-resolved root no longer sits under the prefix (path_starts_with_resolved_root). The refusal prevents a cask from writing outside the managed prefix.","triggerScenarios":"A cask artifact target that resolves to /etc, /Library, ~/, or any path outside $HOMEBREW_PREFIX; a target equal to the prefix with nothing beneath it; a target lexically inside the prefix but reachable through a symlink that resolves elsewhere.","commonSituations":"HOMEBREW_PREFIX changed or differing from what the cask assumed; prefix path components replaced by symlinks (custom installs like ~/homebrew); casks ported from formula that hardcode absolute system paths.","solutions":["Fix the cask artifact stanza so the target is an absolute path under $HOMEBREW_PREFIX (e.g. \"$HOMEBREW_PREFIX/etc/foo.conf\")","Verify HOMEBREW_PREFIX is set and matches the installation being operated on","Resolve symlinked prefix components (use the realpath of $(brew --prefix)) or replace them with real directories","If the file must genuinely live outside the prefix, install it with a different mechanism: generic artifacts cannot escape the prefix"],"exampleFix":"# cask artifact stanza -- before\ntarget: \"/etc/mytool.conf\"\n\n# after\ntarget: \"$HOMEBREW_PREFIX/etc/mytool.conf\"","handlingStrategy":"validation","validationCode":"use std::path::{Path, Component};\n\nfn generic_target_is_valid(target: &Path, prefix: &Path) -> bool {\n    target.is_absolute()\n        && target.starts_with(prefix)\n        && target.strip_prefix(prefix).is_ok_and(|r| r.components().next().is_some())\n        && !target.components().any(|c| matches!(c, Component::ParentDir))\n        && std::fs::canonicalize(target).ok()\n            .zip(std::fs::canonicalize(prefix).ok())\n            .is_some_and(|(t, p)| t.starts_with(p))\n}","typeGuard":"fn is_beneath_homebrew_prefix(target: &std::path::Path) -> bool {\n    let prefix = homebrew_prefix();\n    target.starts_with(&prefix)\n        && target.strip_prefix(&prefix).is_ok_and(|r| r.components().next().is_some())\n}","tryCatchPattern":"match copy_generic_artifact(&artifact) {\n    Err(e) if e.to_string().contains(\"outside Homebrew prefix\") => {\n        // rewrite the target under $HOMEBREW_PREFIX in the cask stanza, then retry\n        return Err(e.wrap_err(\"target must live under $HOMEBREW_PREFIX\"));\n    }\n    other => other?,\n}","preventionTips":["Always express generic artifact targets with the $HOMEBREW_PREFIX variable, never absolute system paths","Keep HOMEBREW_PREFIX stable across installs and upgrades","Avoid symlinking the prefix itself"],"tags":["brew-cask","generic-artifact","path-validation","homebrew-prefix"],"backgroundTag":"path-traversal-guard","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}