Skip to content

Fix the Computer Use Gatekeeper prompt that returned after every update on macOS 26.4 - #13819

Merged
austinywang merged 3 commits into
mainfrom
13803-helper-quarantine-regression
Sep 23, 2026
Merged

austinywang merged 3 commits into
mainfrom
13803-helper-quarantine-regression

Conversation

@austinywang

@austinywang austinywang commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Closes #13803. Supersedes #13602 and re-fixes #13430.

Problem

On macOS 26.4.1, seconds after a Sparkle update relaunches cmux, Gatekeeper shows the first-open dialog for "cmux Computer Use" ("downloaded on an unknown date"). The app bundle carries no quarantine at all. The staged helper copy under ~/Library/Application Support/cmux/cmux-cua/helper/<scope>/ carries 0200;00000000;; on every directory and 0200;<copy time>;; on every file: a record with no agent and no download UUID.

#13602 releases the copied helper by assigning URLResourceValues.quarantineProperties = nil on every entry. That call is not a removal primitive. It sets quarantine properties, and what it does with nil depends on the macOS release. Replayed with a standalone script against the same Foundation, on a FileManager.copyItem copy of a clean tree and of a tree with a real web-download record:

macOS copy of a clean bundle (Sparkle, approved DMG, dev builds) copy of a quarantined bundle (Homebrew, #13430)
15.7.4 (24G517) and 15.7.9 (24G830) throws NSCocoaErrorDomain 512 (underlying ioErr -36) on the first entry; installHelper returns nil and the helper is not staged at all record removed
26.4.1 (25E253) writes 0200;00000000;; on directories and 0200;<now>;; on files replaced by that same empty record
26.6.2 (25G83) and 27.0 (26A428) no-op record removed

(15.7.4, 26.4.1 and 27.0 from my replays; 15.7.9 and 26.6.2 from @teamleaderleo's GitHub-hosted runs of this PR's commits, linked below.)

On 26.4.1 the setter builds a record out of the nil dictionary: flags 0x0200 (LSQuarantineIsOwnedByCurrentUser; the process resolves the file owner through the Membership API while doing it), a timestamp of now for files and 0 for directories, no agent, no UUID. NSURL.setResourceValue(NSNull()) and setResourceValue(nil) do the same; an empty dictionary [:] writes a full default 0281;<now>;;<UUID> record instead, so nil is its own code path, not "empty properties". LaunchServices treats any record as quarantine: syspolicyd evaluates the helper (GK evaluateScanResult: 0 … (id: com.cmuxterm.cua)), CoreServicesUIAgent raises GKQuarantineResolver, and the helper stays held in exec until the Open click sets the user-approved bit (0240). Every update re-stages the helper because its contents change, so the dialog returns after every update on every install type. For Homebrew installs the real download record is replaced by the empty one, which still prompts, so #13430 was not fixed either.

Why it shipped: the swift-package-tests job was skipped on #13602 (it only runs with the full-ci label), and the test asserted through Foundation's quarantineProperties read-back, which is derived from the record. On 26.4.1 that read-back returns [LSQuarantineIsOwnedByCurrentUser, LSQuarantineTimeStamp] after the call, so the #13602 test fails on this OS (reproduced: 4 issues). On 15.x and 27.0 it passes because the setter does remove an existing record, and the test only ever exercised that case. Foundation's view cannot stand in for the attribute Gatekeeper reads.

Fix

ComputerUseHelperQuarantineRelease (new file) walks the copied tree with lstat, probes each entry with getxattr(2), and removes com.apple.quarantine with removexattr(path, name, XATTR_NOFOLLOW), the primitive Sparkle's SUFileManager releaseItemFromQuarantineAtRootURL uses. Symbolic links are neither followed nor modified, entries without the attribute are not touched, and the Foundation setter is never called. The pass continues past an entry it cannot change and reports it. ComputerUseRuntimeService.releaseCopiedHelperFromQuarantine logs each failure (file name and errno, via os.Logger) and keeps the copy.

Two call sites share that path: staging (installHelper, on the temporary copy before it moves into place) and the reuse branch of ensureStandaloneHelperInstalledWithinLifecycle, which releases an already-staged copy in place so a helper staged by the affected nightly is healed at the next launch without restaging or restarting the daemon.

Resulting behavior: a fresh stage from a clean bundle and from a quarantined bundle both end with zero com.apple.quarantine entries and no Gatekeeper dialog, and on macOS 15 staging a clean bundle no longer fails.

Trade-offs

  • A failed removal is logged and non-fatal (Sparkle's choice as well). Making it fatal would trade a one-click dialog for a silently missing helper.
  • A symbolic link's own attribute is left alone and links are never traversed. The shipped helper bundle has 15 entries and no symlinks, so this is a safety margin, not a behavior anyone depends on.
  • The in-place release on the reuse branch costs one getxattr per entry at startup (15 syscalls) inside the existing detached check task; it only writes when an entry actually carries the attribute.
  • installHelper went from private to internal so the tests cover the real staging path (copy, release, move), not only the primitive.
  • No user-facing strings changed (the only new text is an os_log error line), so no localization entries were added; no shortcuts changed.

Tests

  • Commit 8154c89 (tests only): rewrites the package test to apply quarantine with setxattr and assert with getxattr, through both the release call and installHelper, for a quarantined source and a clean source. Against the unfixed code: macOS 26.4.1: 4 of 4 fail (29 issues); macOS 15.7.4: 2 of 4 fail (clean source: Cocoa error 512 from the setter, and staging returns nil).
  • Commit 83850f4 (fix): adds the type and its unit tests (reported entries, a clean tree left untouched, symlinks to an outside file and directory, an immutable entry that cannot be released, task cancellation). 12 tests in 3 suites pass on 26.4.1 and on 15.7.4.
  • Merge commit 86073ec brings in main (including ci: bound the suite coverage gate #13820, which fixed a main-side CI guard); the package suite passes on the merged tree with no warnings in the package.

Ran: swift test --package-path Packages/macOS/CmuxComputerUse on this Mac (26.4.1, Swift 6.3.3) at both commits and on an AWS macOS 15.7.4 builder at both commits; python3 scripts/swift_file_length_budget.py (pass; the runtime service file shrank from 2146 to 2145 lines); python3 scripts/check-package-resolved-policy.py (OK). CI runs with the full-ci label so macos / swift-package-tests is a real gate here: this head first ran with the full-ci label: macos / swift-package-tests passed (so did compile admission, release-build and release-admission). The app-host unit tests shards failed on tests this PR does not touch (GhosttyTerminalViewVisibilityPolicyTests, WorkspacePanelGitBranchTests, WorkspaceRenameShortcutDefaultsTests, ZshShellIntegrationHandoffTests); all six shards also fail on main's last two full-suite runs and ci-status is red there (#13707). Since that lane cannot go green on any PR right now, the label was removed afterwards and the PR is judged under the repository's standard compile-only PR policy like every other PR; the package-test evidence above stays on record in that first run. Final: ci-status green on run 35809550189 (compile admission reused for the unchanged input fingerprint)..

Real app verification (tagged Debug build of 86073ec, cmux DEV issue-13803-helper-quarantine-regression, macOS 26.4.1)

Staged scope for this tag: helper/issu-cc22b4f14211e7ee. Nothing else under helper/ was touched.

  1. Fresh stage from a clean bundle. With no staged directory for the tag, launching the app staged the helper (15 entries). xattr -lr on the staged copy: 0 com.apple.quarantine entries (only com.apple.provenance, which every file written under provenance tracking gets). Both helper daemons started from that copy. Unified log for the launch: the app opened com.apple.coreservices.quarantine-resolver as LaunchServices always does, syspolicyd logged GK evaluateScanResult: 2 … (id: com.cmuxterm.cua), and no GKQuarantineResolver line was logged (the nightly failure logged evaluateScanResult: 0 followed by GKQuarantineResolver initWithProperties). System Events reported no CoreServicesUIAgent window.

  2. Homebrew case. Wrote a web-download record (0081;<time>;Chrome;<UUID>) on all 15 entries of the helper bundled inside the tagged app, deleted the staged directory again, relaunched. The new staged copy: 0 of 15 entries quarantined; the bundled source still carries its record on all 15 entries (only the copy is released). Same log shape: evaluateScanResult: 2, no GKQuarantineResolver, no CoreServicesUIAgent window, both daemons running. The bundled helper was restored to clean afterwards.

  3. In-place heal of an already-staged copy (the state the affected nightly leaves behind). Wrote the nightly's exact records onto this tag's staged copy (0200;00000000;; on directories, 0200;6ab30fce;; on files, 15 entries), quit the app, relaunched through reload.sh --launch. The copy was judged current and released in place: same inode before and after (1143313763, so no restage), 0 of 15 entries quarantined, both daemons running, no GKQuarantineResolver, no CoreServicesUIAgent window.

  4. Control on the same Mac. At 18:35, while this build was running, another agent's DEV build of unfixed main re-staged its own helper into helper/issu-928b8f80211aa4f1. That copy carries 0200;00000000;; on the bundle directory and 0200;<copy time>;; on the executable, syspolicyd logged GK evaluateScanResult: 0 for it, and CoreServicesUIAgent raised GKQuarantineResolver three times until its helper was approved. Same Mac, same minute, same helper binary; only the release code differs. (Looked at read-only; that directory belongs to another build.)

Evidence files (xattr listings before/after, unified-log excerpts, daemon lists) are kept on the reporter's Mac under ~/.local/share/cmux-hq/issue-evidence/helper-quarantine-13803-fix/. A screenshot could not be taken from the agent shell (no Screen Recording grant for its responsible app), so the "no dialog" evidence is the unified log plus the System Events window enumeration; the tagged app is left running for a manual look.

Not done: the build-fleet controller was unreachable from this Mac for the whole session (cmux-ci status timed out; feedback spooled locally), so there is no exact-SHA fleet build or HQ opener link. The verification above used a local ./scripts/reload.sh --tag issue-13803-helper-quarantine-regression --launch build of the pushed merge commit at the user's request.

🤖 Generated with Claude Code

austinywang and others added 2 commits September 22, 2026 17:31
… attribute

The #13602 test checked Foundation's quarantineProperties read-back, which
is derived from the record and differs between macOS releases. Gatekeeper
reads the com.apple.quarantine extended attribute itself, so these tests
apply it with setxattr(2) and assert with getxattr(2), through both the
release call and the real staging path (installHelper, widened from
private to internal for the test). They cover a quarantined source and a
clean source; on macOS 26.4.1 the clean source ends up with an empty
record, which is what brings the Gatekeeper dialog back after every
update (#13803).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…vexattr

`URLResourceValues.quarantineProperties = nil` is not a removal
primitive. It writes a quarantine record, and what it writes for nil
depends on the macOS release: 26.4.1 stores an empty record
(`0200;<time>;;` on files, `0200;00000000;;` on directories) that still
triggers the Gatekeeper first-open dialog, 15.7.4 throws an I/O error on
an entry that carries no record (so staging a clean bundle fails
outright), and 27.0 removes it. Every update re-stages the helper, so
the dialog came back after every update on every install type, and the
Homebrew record from #13430 was only replaced by the empty one.

Gatekeeper reads the raw `com.apple.quarantine` attribute, so the new
`ComputerUseHelperQuarantineRelease` probes each entry with getxattr(2)
and removes the attribute with removexattr(2) and XATTR_NOFOLLOW, the
way Sparkle's SUFileManager does. Symbolic links are neither followed
nor modified, entries without the attribute are left alone, and a
failed removal is logged and reported rather than aborting the stage.
The reuse branch releases an already-staged copy in place so helpers
staged by the affected nightly are healed without restaging.

Supersedes #13602 and re-fixes #13430. Closes #13803.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@austinywang austinywang added the full-ci EXPENSIVE: full macOS tests/builds; overrides selective PR routing. Not needed for normal checks. label Sep 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 0d26d4e2-adf3-4f50-a0c5-d1e68a012e22

📥 Commits

Reviewing files that changed from the base of the PR and between 83a6294 and 83850f4.

📒 Files selected for processing (6)
  • Packages/macOS/CmuxComputerUse/Sources/CmuxComputerUse/ComputerUseHelperQuarantineRelease.swift
  • Packages/macOS/CmuxComputerUse/Sources/CmuxComputerUse/ComputerUseRuntimeService.swift
  • Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/ComputerUseHelperQuarantineReleaseTests.swift
  • Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/ComputerUseRuntimeServiceTests.swift
  • Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/HelperBundleFixture.swift
  • Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/TestQuarantineAttribute.swift

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

The change adds raw quarantine-attribute removal for helper bundle trees, integrates it into helper staging, logs removal failures, and adds tests for traversal, symlinks, cancellation, failures, and clean or quarantined sources.

Changes

Quarantine release

Layer / File(s) Summary
Quarantine walker and report
Packages/macOS/CmuxComputerUse/Sources/CmuxComputerUse/ComputerUseHelperQuarantineRelease.swift
Adds quarantine-attribute inspection and removal using getxattr and removexattr with XATTR_NOFOLLOW. The walker skips symbolic links, continues after failures, reports errno values, and throws on cancellation.
Runtime staging integration
Packages/macOS/CmuxComputerUse/Sources/CmuxComputerUse/ComputerUseRuntimeService.swift
Uses the new release type during helper installation and when an existing destination is current. Failure reports are written to unified logging, and installHelper is internal.
Traversal and staging validation
Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/*
Adds bundle and raw-attribute test helpers. Tests cover quarantined entries, clean trees, symlinks, immutable files, cancellation, and staging from clean or quarantined bundles.

Priority: ⬆️ High

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: High

Sequence Diagram(s)

sequenceDiagram
  participant RuntimeService
  participant ReleaseHelper
  participant FileSystem
  participant UnifiedLog
  RuntimeService->>ReleaseHelper: release(treeAt: destination)
  ReleaseHelper->>FileSystem: inspect entries with getxattr
  ReleaseHelper->>FileSystem: remove attributes with removexattr
  FileSystem-->>ReleaseHelper: Report
  ReleaseHelper-->>RuntimeService: return released URLs and failures
  RuntimeService->>UnifiedLog: log failed removals
Loading

Merge Risk: 🔵 Low · up to 83850

A narrow same-user race can make cleanup affect files outside the helper tree. The PR is otherwise mergeable with this risk explicitly accepted or addressed.

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 6 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies the coding requirements in [#13803] and supports the related behavior in [#13430]. ComputerUseHelperQuarantineRelease checks and removes the raw com.apple.quarantine attribute wit…
Out of Scope Changes check ✅ Passed The changes stay within the linked issue scope. The new quarantine-release type, failure reporting, logging, helper-reuse cleanup, and shared test fixtures directly support removal and verification of…
Cmux Cloud Persistent Session And Early Input ✅ Passed PASS: The PR changes only macOS Computer Use quarantine handling and its tests. The authoritative diff contains six CmuxComputerUse files and no Cloud terminal creation, cmux-tui transport, Ghostt…
Cmux Swift Actor Isolation ✅ Passed PASS: The production changes do not introduce a listed actor-isolation mistake. The new file-scoped logger is explicitly nonisolated. ComputerUseHelperQuarantineRelease is a synchronous filesystem…
Cmux Swift Blocking Runtime ✅ Passed PASS. The production diff adds synchronous filesystem traversal and getxattr/removexattr calls, but it adds no semaphore, blocking wait, sleep, delayed dispatch, polling loop, main-queue sync, tim…
Cmux Browser Automation Off-Main ✅ Passed PASS: The PR changes only Computer Use quarantine/runtime and test files. It does not modify Sources/TerminalController.swift, ControlCommandExecutionPolicy.swift, or policy tests. The patch adds …
Cmux Expensive Synchronous Load ✅ Passed The diff adds a synchronous directory walk with lstat, getxattr, and removexattr, but it does not add an agent-history or large JSON loader. Both production call paths run this work inside `Task…
Cmux Cache Substitution Correctness ✅ Passed PASS: The production diff does not replace an authoritative read with a cache in a persistence, history, undo, or snapshot path. It adds fresh lstat/getxattr checks and removexattr cleanup for t…
Cmux No Hacky Sleeps ✅ Passed PASS — The pull request changes only Swift source and test files. The custom check applies only to non-Swift TypeScript, JavaScript, shell, or build/runtime script changes. No applicable changed files…
Cmux Algorithmic Complexity ✅ Passed The changed production code performs one recursive traversal of the helper bundle. Each entry receives constant-time lstat and getxattr work, with at most one removexattr; directory contents are…
Cmux Swift Concurrency ✅ Passed The diff does not introduce a prohibited concurrency pattern. The new quarantine-release type is synchronous and uses no Dispatch queues, Combine, completion handlers, or fire-and-forget tasks. The ru…
Cmux Swift @Concurrent ✅ Passed No concurrency-rule violation is introduced. The new quarantine walk and releaseCopiedHelperFromQuarantine are synchronous and nonisolated. The file-heavy release call added to the @MainActor li…
Cmux Swift Package Boundaries ✅ Passed PASS. The production changes stay inside the existing SwiftPM package target CmuxComputerUse at Packages/macOS/CmuxComputerUse/Sources/CmuxComputerUse. The new quarantine type uses only Darwin a…
Cmux Swiftpm Lockfiles ✅ Passed PASS. The PR changes only CmuxComputerUse source and test files. It does not change any Package.swift, Package.resolved, .gitignore, or Xcode project package-reference files. The CmuxComputerUse manif…
Cmux Swift Logging ✅ Passed The only new production diagnostic is logger.error in ComputerUseRuntimeService.swift. The file-scoped logger is declared nonisolated private let, so it complies with the MainActor isolation rul…
Cmux User-Facing Error Privacy ✅ Passed The production diff adds only an internal os.Logger error for quarantine-release failures. The call sites use it during helper staging and reuse; no changed path forwards this text to cmux UI, CLI o…
Cmux Full Internationalization ✅ Passed PASS. The production diff adds quarantine-handling code and one unified logging diagnostic. It adds no user-facing Swift UI, alert, menu, command, metadata, web text, catalog entry, or locale change. …
Cmux Swiftui State Layout ✅ Passed The pull request does not add or modify SwiftUI views or SwiftUI state. The changed files use Foundation, Darwin, Testing, and os APIs for quarantine handling and tests. No changed diff contains Obser…
Cmux Architecture Rethink ✅ Passed The diff is a small correctness fix with a clear invariant: staged helper copies must not carry com.apple.quarantine. It replaces the incorrect Foundation setter with the required getxattr/`remove…
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The PR changes quarantine handling, runtime staging, and tests only. The reviewed diff introduces no NSWindow, NSPanel, NSWindowController, SwiftUI Window, WindowGroup, close-shortcut routing, o…
Cmux Source Artifacts ✅ Passed All six changed paths are intentional Swift source or test infrastructure under Packages/macOS/CmuxComputerUse. The new HelperBundleFixture and TestQuarantineAttribute files create required temp…
Cmux No Test Or Debug Seam In Production Source ✅ Passed PASS. The production diff adds no #if DEBUG or test-build guard, no test/debug-named member, and no wrapper accessor for private state. ComputerUseHelperQuarantineRelease is production functionali…
Title check ✅ Passed The title clearly identifies the primary change: fixing the recurring Computer Use Gatekeeper prompt after macOS updates.
Description check ✅ Passed The description is detailed and directly covers the problem, implementation, trade-offs, automated tests, and real-app verification. It does not include the template's explicit Demo Video, Review Trig…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Ran the immutable tests-only commit 8154c89 and current fix head 86073ec through the whole CmuxComputerUse package on two standard GitHub-hosted macOS runners in my personal fork: run 35805814496.

Actual runtime Tests-only commit Fixed head
macOS 15.7.9 (24G830) 6 tests, 2 failures 12 passed
macOS 26.6.2 (25G83) 6 passed 12 passed

The macOS 15 failures were the clean-helper nil quarantine setter (Cocoa error 512) and clean-helper staging returning nil. The newer macOS 26.6.2 image did not reproduce the reported 26.4.1 behavior, so this is additional cross-runtime evidence, not a replacement for the author's 26.4.1 reproduction. Runtime and SDK/toolchain metadata, original command exits, counts, and raw logs are retained in the run artifacts. This was package-only: no app build, signing, or Gatekeeper launch claim.

@austinywang austinywang removed the full-ci EXPENSIVE: full macOS tests/builds; overrides selective PR routing. Not needed for normal checks. label Sep 23, 2026
@austinywang

Copy link
Copy Markdown
Contributor Author

Thanks, that closes the matrix nicely: 15.7.9 fails the same two clean-helper cases (Cocoa 512, staging nil) as my 15.7.4 builder run, and 26.6.2 behaving like 27.0 (no-op on a clean copy) narrows the empty-record write to the 26.4.x line. I folded both rows into the OS table in the description with a link to your run.

One more data point from the tagged-build verification on the 26.4.1 Mac: while the fixed DEV build was running, another agent's DEV build of unfixed main re-staged its own helper in the same minute and got 0200;00000000;; on the bundle plus a GKQuarantineResolver prompt, so the control case is on the same machine and helper binary.

@austinywang
austinywang merged commit 4849743 into main Sep 23, 2026
73 of 84 checks passed
@austinywang

Copy link
Copy Markdown
Contributor Author

Review audit against HEAD 86073ec (re-checked after the final CI run):

comment id author file:line ask disposition commit
5786932870 coderabbitai[bot] (top level) none; auto-generated summary, no review threads posted already-fixed (nothing to change) 86073ec
5787366955 teamleaderleo (top level) none; contributes macOS 15.7.9 / 26.6.2 package-test evidence already-fixed: rows folded into the OS table in the description; replied in 5787780052 86073ec
cubic check cubic — no findings posted — —
Codex / Greptile — — no review posted — —

Inline review threads: none. CHANGES_REQUESTED: none. Required checks (CLA Assistant, CLA policy guard, ci-status, Web complexity, web-validation) are green on run 35809550189; the three red check runs still attached to this commit belong to the cancelled full-ci run (app-host unit test shards that also fail on main, #13707) and are not required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants