Skip to content

fix(desktop): stop looping macOS TCC permission prompts#2745

Open
D3OXY wants to merge 1 commit into
pingdotgg:mainfrom
D3OXY:fix/repeated-tcc-permission-prompts
Open

fix(desktop): stop looping macOS TCC permission prompts#2745
D3OXY wants to merge 1 commit into
pingdotgg:mainfrom
D3OXY:fix/repeated-tcc-permission-prompts

Conversation

@D3OXY
Copy link
Copy Markdown
Contributor

@D3OXY D3OXY commented May 18, 2026

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:

  • Tailscale, "Other apps" category (issues 2254, 2737). DesktopServerExposure.getAdvertisedEndpoints unconditionally spawned tailscale status whenever 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.
  • Path-autocomplete, Music / Documents / Downloads / Desktop categories (issues 2254, 2088). CommandPalette prefetched the highlighted browse child on every render (useEffect keyed on highlightedBrowseEntry). Each prefetch sent partialPath: "~/Music/" (with trailing separator from appendBrowsePathSegment) to WorkspaceEntries.browse, which readdir'd the gated dir server-side. Arrow-keying past Music, Documents, Downloads, Desktop, Movies, Pictures in the home folder fired one TCC prompt per highlight change.
  • Denied TCC prompt re-fires. WorkspaceEntries.browse surfaced EACCES/EPERM from readdir as a WorkspaceEntriesBrowseError, which the renderer's React Query then re-attempted on retry — re-firing the prompt.

Changes

  • apps/desktop/src/backend/DesktopServerExposure.ts — early-return from getAdvertisedEndpoints with only the core endpoints when mode !== "network-accessible" and tailscaleServeEnabled === false. Wrap readTailscaleStatus in Effect.cachedWithTTL (60s) so even when the gate passes, panel remounts don't re-spawn the CLI.
  • apps/desktop/src/backend/tailscaleEndpointProvider.ts — accept an optional readMagicDnsName Effect 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 one readdir when they actually navigate into a directory instead of one per arrow-key press.
  • apps/server/src/workspace/Layers/WorkspaceEntries.ts — catch EACCES/EPERM from readdir in browse and return an empty listing so a denied prompt stops re-firing on retry.

Tests

  • DesktopServerExposure.test.ts: new test using a "die on spawn" ChildProcessSpawner layer asserts that getAdvertisedEndpoints does not invoke the spawner while the server is in local-only mode with Tailscale Serve off.
  • tailscaleEndpointProvider.test.ts: new test verifies the readMagicDnsName injection bypasses readTailscaleStatus and produces the magic-DNS endpoint as expected.
  • WorkspaceEntries.test.ts: new test mocks fsPromises.readdir to reject with EACCES and asserts browse returns { parentPath, entries: [] } instead of erroring.

Test plan

  • bun typecheck clean
  • bun lint clean (no new warnings)
  • bun fmt clean
  • bun run test — 1023 passed, 4 skipped (matches main), including the three new tests
  • On macOS with Mac App Store Tailscale installed: open Settings → Connections without enabling Network Access, confirm no "Other apps" prompt appears
  • With Network Access enabled, confirm the prompt fires at most once per 60s instead of on every panel mount
  • In the command palette browse mode, arrow-key past Music / Documents / Downloads and confirm no TCC prompts fire until you press / to navigate into one

Note: 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

  • getAdvertisedEndpoints in DesktopServerExposure.ts now early-returns without spawning the tailscale CLI when the mode is not network-accessible and tailscaleServeEnabled is false, preventing repeated macOS TCC permission prompts.
  • When tailscale resolution is needed, magic DNS name reads are cached for 60 seconds via Effect.cachedWithTTL.
  • resolveTailscaleAdvertisedEndpoints in tailscaleEndpointProvider.ts accepts an optional injected readMagicDnsName effect, falling back to the default readTailscaleStatus approach.
  • WorkspaceEntries.browse now returns an empty listing instead of throwing on EACCES/EPERM errors, silently handling OS-denied directory access.

Macroscope summarized bfd7f5a.

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cba87a4e-15b6-4c8d-97dc-15860f7e4f48

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels May 18, 2026
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented May 18, 2026

Approvability

Verdict: 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.

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

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Q] Keeps asking for folder permissions every X hours/randomly

1 participant