cask/staged: check App Management permissions before chown#22967
Merged
MikeMcQuaid merged 1 commit intoJul 5, 2026
Conversation
`uninstall_preflight { set_ownership ... }` runs `sudo chown -R` on the
app bundle. On macOS 13 or later, modifying the contents of an app
bundle requires the App Management TCC permission even when running as
root, and `chown` does not trigger the system permission prompt:
without the permission, the uninstall aborts with one
`Operation not permitted` line per file in the bundle and no hint at
the actual cause.
Reuse Quarantine.app_management_permissions_granted? (already used for
upgrades in Cask::Artifact::Moved) before chowning, so that:
- the system permission prompt is triggered, letting the user grant
access on the spot, and
- a clear, actionable CaskError is raised if the permission is missing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
brewcommands to reproduce the bug?brew lgtm(style, typechecking and tests) locally?This PR was prepared with the assistance of Claude Code, driven and reviewed by the account owner. The bug was diagnosed and the fix verified on a real affected system (macOS 15, Apple Silicon, Tunnelblick 8.0.2 installed via the
tunnelblickcask); every command and test run quoted below was actually executed on that machine.What does this PR do?
Cask::Staged#set_ownership(used by casks viauninstall_preflight { set_ownership ... }) now checksQuarantine.app_management_permissions_granted?for each path before runningsudo chown -R. If the App Management permission is missing, it raises aCaskErrorwith actionable guidance instead of aborting with one rawchown: ...: Operation not permittedline per file inside the app bundle. The check itself also triggers the macOS App Management permission prompt (a plainchownnever does), so in the common case the user can simply approve the prompt and re-run the command.Why is this needed? (root cause)
On macOS 13 or later, modifying the contents of an app bundle requires the App Management TCC permission (
kTCCServiceSystemPolicyAppBundles) — even when running as root viasudo. Homebrew already knows this:Cask::Quarantine.app_management_permissions_granted?exists for exactly this reason and is wired into the upgrade path (Cask::Artifact::Moved). Butset_ownershipis not covered, sobrew uninstallof a cask whose app is root-owned fails hard when the terminal lacks the permission.The
tunnelblickcask is the prime example: Tunnelblick "secures" itself on first launch (its privileged helper chowns the whole bundle toroot:wheel), which is why the cask usesuninstall_preflight { set_ownership "#{appdir}/Tunnelblick.app" }. On an affected machine,brew uninstall tunnelblickprints ~620 lines ofand aborts with
Error: Failure while executing; /usr/bin/sudo -E -- chown -R -- user:staff /Applications/Tunnelblick.app exited with 1, giving the user no hint that a privacy setting (not file permissions) is the cause.Root-cause evidence gathered on the affected machine:
EPERM("Operation not permitted"), notEACCES("Permission denied"), and happens even as root — so it is not a DAC/ownership problem.uchg/schgflags (ls -lOis clean), ruling out immutable flags.touch /Applications/Tunnelblick.app/Contents/Resources/x) fails withOperation not permitted, while creating a file directly in/Applicationssucceeds for the same user — the TCC App Management signature.Quarantine.app_management_permissions_granted?was introduced for (see its comments inLibrary/Homebrew/cask/quarantine.rb): App Management has been required for modifying other apps' bundles since macOS 13 Ventura, and only an actual write attempt — notchown— makes macOS show the permission prompt.Steps to reproduce
On macOS ≥ 13, in a terminal that does not have App Management permission:
brew install --cask tunnelblickroot:wheel).brew uninstall tunnelblickBefore this change: hundreds of
chown: ...: Operation not permittedlines, then a genericError: Failure while executing. No macOS permission prompt is shown, becausechowndoes not trigger one.After this change:
(and macOS shows the App Management prompt for the terminal, so approving it and re-running succeeds.)
The error wording follows the existing App Management message in
Library/Homebrew/cask/quarantine.rb.Testing
test/cask/dsl/shared_examples/staged.rb(exercised via thepreflight,postflightanduninstall_preflightspecs): permission granted →chownruns as before; permission missing →CaskErrormentioning App Management and nochownattempt. All three spec files pass.srb tc: no errors.brew styleon the changed files: no offenses.brew tests --changedselects no specs for these files (shared examples are not mapped to their consumers), so the three consumer spec files were run explicitly instead — a superset of whatbrew lgtmruns.tunnelblickcask and the real root-owned/Applications/Tunnelblick.app, withSystemCommandstubbed to fail exactly like a TCC-deniedtouch: the newCaskErroris raised. A stubbed plainsudofailure (sudo: a terminal is required to read the password) still propagates unchanged, so genuinesudoproblems are not misattributed to App Management.Existing
set_ownershipbehaviour is unchanged whenever the permission check passes (including non-directory paths, for whichapp_management_permissions_granted?returnstrueimmediately).🤖 Generated with Claude Code