fix(desktop): stop looping macOS TCC permission prompts#2745
Conversation
Three independent code paths were each retriggering macOS TCC prompts on every render or panel mount, with no caching or enable-gate. - DesktopServerExposure.getAdvertisedEndpoints unconditionally spawned `tailscale status` on every renderer call, even when the user had not enabled any network exposure. On Mac App Store Tailscale the CLI lives in Tailscale's sandbox container, so each spawn re-fires the "Other apps" TCC prompt. Gate the spawn behind `network-accessible || tailscaleServeEnabled` and cache the result for 60s via Effect.cachedWithTTL. - CommandPalette prefetched the highlighted browse child on every arrow-key press, which readdir's the target dir server-side. When the highlighted entry is `~/Music`, `~/Documents`, `~/Downloads` etc., that re-fires the corresponding TCC prompt every keystroke. Drop the child prefetch; keep the parent prefetch for back-nav. - WorkspaceEntries.browse now swallows EACCES/EPERM from readdir and returns an empty listing, so a denied TCC prompt no longer surfaces as a re-tried error.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
ApprovabilityVerdict: Approved Straightforward bug fix that prevents repeated macOS TCC permission prompts through caching, early-exit gates, and graceful error handling. Changes are defensive in nature with clear comments and test coverage. You can customize Macroscope's approvability policy. Learn more. |
Summary
Fixes #2254, #2737, #2088 — three issues with the same shape: macOS TCC permission prompts ("Other apps", "Music", "Documents", "Downloads", etc.) re-fire repeatedly because three different code paths trigger fresh file access on every render or panel mount, with no caching or enable-gate.
Repro / root causes:
DesktopServerExposure.getAdvertisedEndpointsunconditionally spawnedtailscale statuswhenever the Connections settings panel mounted, even when the user had not opted into any network exposure. On Mac App Store Tailscale the CLI lives inside Tailscale's sandbox container, so each spawn re-fires the "T3 Code would like to access data from other apps" TCC prompt. Issue 2737's reproduction (enable Network Access → prompt loops every few seconds) maps directly to the Settings panel re-firing this effect.CommandPaletteprefetched the highlighted browse child on every render (useEffectkeyed onhighlightedBrowseEntry). Each prefetch sentpartialPath: "~/Music/"(with trailing separator fromappendBrowsePathSegment) toWorkspaceEntries.browse, whichreaddir'd the gated dir server-side. Arrow-keying pastMusic,Documents,Downloads,Desktop,Movies,Picturesin the home folder fired one TCC prompt per highlight change.WorkspaceEntries.browsesurfacedEACCES/EPERMfromreaddiras aWorkspaceEntriesBrowseError, which the renderer's React Query then re-attempted on retry — re-firing the prompt.Changes
apps/desktop/src/backend/DesktopServerExposure.ts— early-return fromgetAdvertisedEndpointswith only the core endpoints whenmode !== "network-accessible"andtailscaleServeEnabled === false. WrapreadTailscaleStatusinEffect.cachedWithTTL(60s) so even when the gate passes, panel remounts don't re-spawn the CLI.apps/desktop/src/backend/tailscaleEndpointProvider.ts— accept an optionalreadMagicDnsNameEffect so the caller can inject the cached reader. Default path is unchanged.apps/web/src/components/CommandPalette.tsx— drop the highlighted-child prefetch; keep the parent prefetch for back-navigation. The user pays onereaddirwhen they actually navigate into a directory instead of one per arrow-key press.apps/server/src/workspace/Layers/WorkspaceEntries.ts— catchEACCES/EPERMfromreaddirinbrowseand return an empty listing so a denied prompt stops re-firing on retry.Tests
DesktopServerExposure.test.ts: new test using a "die on spawn"ChildProcessSpawnerlayer asserts thatgetAdvertisedEndpointsdoes not invoke the spawner while the server is inlocal-onlymode with Tailscale Serve off.tailscaleEndpointProvider.test.ts: new test verifies thereadMagicDnsNameinjection bypassesreadTailscaleStatusand produces the magic-DNS endpoint as expected.WorkspaceEntries.test.ts: new test mocksfsPromises.readdirto reject withEACCESand assertsbrowsereturns{ parentPath, entries: [] }instead of erroring.Test plan
bun typecheckcleanbun lintclean (no new warnings)bun fmtcleanbun run test— 1023 passed, 4 skipped (matches main), including the three new testsMusic/Documents/Downloadsand confirm no TCC prompts fire until you press/to navigate into oneNote: I do not have a Mac App Store Tailscale install to verify the macOS prompt behavior end-to-end; the fix targets the code paths identified in the issues and is covered by unit tests for the gating, caching, and EACCES handling.
Note
Stop spawning tailscale CLI when exposure mode is local-only on macOS
getAdvertisedEndpointsinDesktopServerExposure.tsnow early-returns without spawning the tailscale CLI when the mode is not network-accessible andtailscaleServeEnabledis false, preventing repeated macOS TCC permission prompts.Effect.cachedWithTTL.resolveTailscaleAdvertisedEndpointsintailscaleEndpointProvider.tsaccepts an optional injectedreadMagicDnsNameeffect, falling back to the defaultreadTailscaleStatusapproach.WorkspaceEntries.browsenow returns an empty listing instead of throwing onEACCES/EPERMerrors, silently handling OS-denied directory access.Macroscope summarized bfd7f5a.