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

merged4
main
GitHub fork: fix/repeated-tcc-permission-prompts
openedMay 18, 2026byDD3OXY
View on GitHubTwo-way sync enabledPR last imported 10d ago

Summary

Fixes pingdotgg/t3code#2254 Fixes pingdotgg/t3code#2737 Fixes pingdotgg/t3code#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 looping macOS TCC permission prompts by skipping tailscale CLI spawns in local-only mode

  • In DesktopServerExposure.ts, adds an early return in getAdvertisedEndpoints that skips all tailscale CLI invocations when the server is not in network-accessible mode and Tailscale Serve is disabled.
  • Tailscale status lookups are now cached for 60 seconds via Effect.cachedWithTTL to reduce spawn frequency when tailscale resolution is needed.
  • resolveTailscaleAdvertisedEndpoints in tailscaleEndpointProvider.ts now accepts an optional injected readMagicDnsName effect, allowing callers to share the cached reader.
  • In WorkspaceEntries.ts, browse now catches EACCES/EPERM errors and returns an empty listing instead of propagating a browse error, preventing repeated TCC permission dialogs on macOS.
  • Child directory prefetching in CommandPalette.tsx is removed; only the parent directory is prefetched during browse navigation.

Macroscope summarized 180835f.


Note

Medium Risk Changes advertised-endpoint and filesystem browse behavior on macOS (empty listings on deny, deferred directory reads); logic is guarded by exposure settings and covered by new tests, but UX when access is denied differs from surfacing errors.

Overview Stops repeated macOS TCC permission prompts by gating and caching sensitive file/process access instead of hitting them on every UI refresh.

Desktop server exposure — getAdvertisedEndpoints now returns only core endpoints when exposure is local-only and Tailscale Serve is off, so the Tailscale CLI is not spawned (avoiding repeated “Other apps” prompts). When Tailscale resolution runs, MagicDNS reads go through a 60s TTL cache via injectable readMagicDnsName.

Command palette browse — Removes prefetch of the highlighted child directory on arrow-key navigation; only the parent path is prefetched for back navigation, so gated home folders are not readdir'd until the user actually navigates in.

Workspace browse API — browse treats EACCES/EPERM from readdir as an empty listing instead of an error, so React Query retries do not re-trigger denied TCC prompts.

Tests cover local-only Tailscale spawn gating, injected MagicDNS reader, and EACCES browse behavior.

Reviewed by Cursor Bugbot for commit 180835f2562447aaf9d9ce6d261d1f593a5f1893. Bugbot is set up for automated code reviews on this repo. Configure here.

Added 1 commit

  • fix(desktop): stop looping macOS TCC permission promptsbfd7f5a
Synced from GitHub

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: 418443b5-bd8d-4050-af7d-a54e1668f472

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.

Synced from GitHub

Approvability

Verdict: Needs human review

This bug fix modifies runtime behavior around Tailscale CLI spawning and error handling. An unresolved review comment identifies a potential regression where Tailscale IP endpoints may be dropped for users with Tailnet interfaces in local-only mode.

You can customize Macroscope's approvability policy. Learn more.

Added 3 commits

  • Merge remote-tracking branch 'upstream/main' into fix/repeated-tcc-permission-promptsf7be35d
  • Merge branch 'main' into fix/repeated-tcc-permission-prompts88aac08
  • Update DesktopServerExposure.ts180835f
CcursorBotreviewed · 1 inline threadJun 5, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 180835f2562447aaf9d9ce6d261d1f593a5f1893. Configure here.

……@@ -516,11 +532,20 @@ const make = Effect.gen(function* () {
538+ // TCC prompt on Mac App Store Tailscale builds.
539+ if (state.mode !== "network-accessible" && !state.tailscaleServeEnabled) {
540+ return coreEndpoints;
541+ }
CcursorBoton line 541Jun 5, 2026

Gate drops Tailscale IP endpoints

Medium Severity

getAdvertisedEndpoints returns only core endpoints when effective mode is local-only and Tailscale Serve is off, skipping all of resolveTailscaleAdvertisedEndpoints. That path still adds Tailscale IP URLs from networkInterfaces without spawning the CLI, so users with a Tailnet interface (or network-accessible preference but LAN-unavailable fallback) no longer see those endpoints in Connections.

Reviewed by Cursor Bugbot for commit 180835f2562447aaf9d9ce6d261d1f593a5f1893. Configure here.