{"record":{"id":"60d06b02de75014f","repo":"jdx/mise","slug":"homebrew-now-owns-this-cask","errorCode":null,"errorMessage":"Homebrew now owns this cask","messagePattern":"Homebrew now owns this cask","errorType":"validation","errorClass":"eyre::Report","httpStatus":null,"severity":"warning","filePath":"src/system/packages/brew/cask.rs","lineNumber":7250,"sourceCode":"        }\n    }\n    for target in &candidate.receipt.targets {\n        if claims\n            .get(&target.path)\n            .is_some_and(|tokens| tokens.iter().any(|token| token != &candidate.token))\n        {\n            bail!(\n                \"artifact target is now claimed by another cask: {}\",\n                target.path.display()\n            );\n        }\n    }\n    Ok(())\n}\n\nfn validate_cask_prune_candidate(candidate: &CaskPruneCandidate) -> Result<()> {\n    if homebrew_metadata_present(&candidate.token) {\n        bail!(\"Homebrew now owns this cask\");\n    }\n    let receipt = &candidate.receipt;\n    if read_receipt(&candidate.version_dir)?.as_ref() != Some(receipt) {\n        bail!(\"ownership receipt has changed\");\n    }\n    if receipt.schema_version != 3 || !receipt.prune_safe || !receipt.pkg_ids.is_empty() {\n        bail!(\"receipt is not marked safe for direct-artifact pruning\");\n    }\n    if !receipt.metadata_only_apps.is_empty() {\n        bail!(\"metadata-only app ownership cannot be proven safely during pruning\");\n    }\n    let records = receipt\n        .targets\n        .iter()\n        .map(|record| (record.path.clone(), record))\n        .collect::<BTreeMap<_, _>>();\n    let expected = receipt\n        .apps","sourceCodeStart":7232,"sourceCodeEnd":7268,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/src/system/packages/brew/cask.rs#L7232-L7268","documentation":"Thrown by mise's Homebrew-cask pruning path (validate_cask_prune_candidate, src/system/packages/brew/cask.rs:7248) when the Caskroom token directory being pruned now contains a `.metadata` directory. mise only prunes casks it installed itself (tracked via a `.mise-cask.toml` receipt); a `.metadata` dir means real Homebrew has since installed or claimed the same cask token. Deleting artifacts would break a brew-managed install, so the prune is refused and the cask is reported in the plan's skipped list instead of removed.","triggerScenarios":"Running mise's package prune/uninstall for a cask token after `brew install --cask <same-token>` (or a brew upgrade/reinstall) created `$(brew --prefix)/Caskroom/<token>/.metadata`. Any call into cask_prune_plan/validate_cask_prune_candidate where homebrew_metadata_present(token) is true.","commonSituations":"User installs a cask with mise, later installs or reinstalls the same cask with native Homebrew, then asks mise to prune; migrating between brew and mise without cleaning the Caskroom; mise and Homebrew sharing one HOMEBREW_PREFIX so token dirs collide.","solutions":["Decide which tool owns the cask: if Homebrew should own it, run `brew uninstall --cask <token>` or stop tracking it in mise config.","If mise should own it, remove Homebrew's claim first with `brew uninstall --cask <token>` (this removes the .metadata dir), then re-run the mise prune.","If the .metadata dir is a stale leftover from an aborted brew operation, verify with `brew list --cask`, then remove `<Caskroom>/<token>/.metadata` manually and re-run prune.","Keep mise and Homebrew on separate prefixes so cask tokens never collide."],"exampleFix":"# before\nmise prune  # skip: \"Homebrew now owns this cask\"\n\n# after (mise should own the cask)\nbrew uninstall --cask <token>\nmise prune","handlingStrategy":"validation","validationCode":"// Check Homebrew's ownership marker before offering/pruning a cask\nfn brew_owns_cask(caskroom: &std::path::Path, token: &str) -> bool {\n    caskroom.join(token).join(\".metadata\").symlink_metadata().is_ok()\n}\n\nif brew_owns_cask(&caskroom, &token) {\n    // skip: let brew manage this cask; do not prune\n}","typeGuard":null,"tryCatchPattern":"Treat the Err as a skip, not an abort: `if let Err(reason) = validate_cask_prune_candidate(&candidate) { plan.skipped.push(CaskPruneSkip { token: candidate.token.clone(), reason: reason.to_string() }); continue; }` — exactly how cask.rs itself converts this refusal into a reported skip.","preventionTips":["Never let Homebrew and mise manage the same cask token under one prefix.","When migrating a cask between brew and mise, uninstall it from the other tool first.","Treat a .metadata dir in a Caskroom token dir as a hard ownership marker — never delete it to force pruning."],"tags":["homebrew","cask","prune","ownership","mise"],"backgroundTag":"package-ownership-conflict","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}