jdx/mise · error
brew-cask: invalid structured flight path
Error message
brew-cask: invalid structured flight path '{}' What it means
validate_flight_relative_path enforces that brew-cask 'flight' (staged artifact) paths stay relative and safe before they are joined to an extraction root. A path is rejected if it is absolute or contains a ParentDir (`..`) component, because either would let an archive or cask definition address files outside the staging directory (path traversal).
Solutions
- Fix the cask's artifact path so it is relative to the extraction root and contains no '..' components (e.g. 'MyApp.app' instead of '/Applications/MyApp.app').
- If targeting an absolute install location is intended, use the artifact's target/dest fields (which support absolute destinations) rather than the source flight path.
- Inspect the offending cask definition/tap for tampering and re-pin to the upstream cask.
- If this fires on a legitimate path, report/normalize the path before calling (strip leading slashes, resolve '..' lexically) and re-validate.
Example fix
// before flight_path = "/Applications/MyApp.app" // after flight_path = "MyApp.app" # relative to extraction root, no '..'
Defensive patterns
Strategy: validation
Validate before calling
use std::path::{Path, Component};
fn is_safe_flight_path(p: &str) -> bool {
let path = Path::new(p);
!path.is_absolute()
&& !path.components().any(|c| matches!(c, Component::ParentDir))
&& !p.is_empty()
}
if !is_safe_flight_path(user_path) {
return Err(format!("flight path must be relative and contain no '..': {user_path}"));
} Type guard
fn safe_relative_path(p: &Path) -> Option<&Path> {
if p.is_absolute() || p.components().any(|c| matches!(c, Component::ParentDir)) { None } else { Some(p) }
} Prevention
- Always define cask artifact sources relative to the archive root.
- Never author cask stanzas with absolute source paths; use the target/dest field for absolute destinations.
- Validate cask JSON with a linter that rejects '..' and absolute sources.
- Treat third-party tap casks as untrusted input and review their paths.
When it happens
Trigger: Any brew-cask install that resolves a flight path via parse_flight_path, parse_permissions_flight_path, expand_staged_glob, resolve_flight_path, or resolve_flight_path_with_context when the cask's artifact/permission path is absolute (starts with '/') or contains a '..' component.
Common situations: Installing a hand-written or third-party cask whose app/binary/preflight stanza references '/Applications/...' or 'foo/../../bar'; a cask stanza edited to target outside the staged archive; corrupted or malicious cask JSON pulled from a nonstandard tap.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- brew-cask: refusing generic artifact source outside the…
- brew-cask: staged symlink path escaped extraction root
- brew-cask: duplicate app target
- brew-cask: invalid app source
- brew-cask: invalid
AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17).
Data as JSON: /api/errors/9229a7ced604c332.
Report an issue: GitHub.
Appendix: source
Thrown at src/system/packages/brew/cask/flight.rs:1207
.replace("{{staged_path}}", &staged_path.to_string_lossy())
.replace("{{appdir}}", &appdir.to_string_lossy());
if let Some(version) = version {
value = value.replace("{{version}}", version);
}
if let Some(rest) = value.strip_prefix("~/") {
value = crate::dirs::HOME.join(rest).to_string_lossy().to_string();
}
value
}
pub(super) fn validate_flight_relative_path(path: &str) -> Result<()> {
let path = Path::new(path);
if path.is_absolute()
|| path
.components()
.any(|component| matches!(component, Component::ParentDir))
{
bail!(
"brew-cask: invalid structured flight path '{}'",
path.display()
);
}
Ok(())
}
pub(super) fn expand_braces(pattern: &str) -> Vec<String> {
let Some(start) = pattern.find('{') else {
return vec![pattern.to_string()];
};
let Some(end_offset) = pattern[start + 1..].find('}') else {
return vec![pattern.to_string()];
};
let end = start + 1 + end_offset;
let prefix = &pattern[..start];
let suffix = &pattern[end + 1..];
let mut expanded = Vec::new();View on GitHub (pinned to 533346cc37)