Skip to content

fix(tailscale): fall back to the macOS app binary when tailscale isn't on PATH - #5145

Open
quinnypig wants to merge 1 commit into
pingdotgg:mainfrom
quinnypig:fix/tailscale-macos-app-path-fallback
Open

fix(tailscale): fall back to the macOS app binary when tailscale isn't on PATH#5145
quinnypig wants to merge 1 commit into
pingdotgg:mainfrom
quinnypig:fix/tailscale-macos-app-path-fallback

Conversation

@quinnypig

@quinnypig quinnypig commented Jul 31, 2026

Copy link
Copy Markdown

What changed

packages/tailscale/src/tailscale.ts: when spawning tailscale fails with ENOENT on macOS, retry once against /Applications/Tailscale.app/Contents/MacOS/Tailscale if that file exists, instead of surfacing the spawn failure immediately. This applies to both readTailscaleStatus (Tailnet/MagicDNS detection) and runTailscaleCommand (serve/serve off). The file-existence check is exposed as an injectable TailscaleExecutableFileCheck (mirrors the existing ClaudeExecutableFileCheck pattern in apps/server/src/provider/Drivers/ClaudeExecutable.ts) so tests can fake the filesystem instead of touching real paths. Added three tests covering the retry, the no-retry-when-binary-absent case, and the no-retry-on-non-macOS case. No behavior change on Linux/Windows or when the app binary isn't installed.

Why

Tailscale's macOS app does not put a tailscale binary on PATH. Their own docs have users work around this with a shell alias (alias tailscale=/Applications/Tailscale.app/Contents/MacOS/Tailscale) or by manually symlinking it into /usr/local/bin. A shell alias only exists inside an interactive shell — it's invisible to child_process.spawn, which does a direct PATH lookup without going through a shell. So on any Mac where the user relies on the alias (rather than a manual symlink), every tailscale spawn in this codebase fails with ENOENT, and that failure is currently swallowed as "not on Tailscale" (e.g. apps/desktop/src/backend/tailscaleEndpointProvider.ts treats the read failure as magicDnsName: null). The user's machine is actually on the Tailnet and tailscale status --json works fine when typed at a prompt — this is purely a subprocess PATH-resolution gap, not a networking issue.

I hit this locally: tailscale resolved via type tailscale as a zsh alias only, and a bare child_process.spawn('tailscale', ...) reproduced SPAWN ERROR: spawn tailscale ENOENT even though tailscale status --json succeeded when run directly in the shell.

Test plan

  • pnpm --filter @t3tools/tailscale typecheck
  • pnpm --filter @t3tools/tailscale test (16/16 passing, including 3 new cases)

Note

Low Risk
Scoped to macOS subprocess resolution with existence-gated fallback; no change to Linux/Windows or when the app binary is absent, and tests cover the new paths.

Overview
On macOS, when spawning tailscale fails with ENOENT, the package now retries once using /Applications/Tailscale.app/Contents/MacOS/Tailscale if that file exists. This covers status reads and serve / serve off commands, so Tailnet detection works when users rely on a shell alias instead of a PATH binary.

File existence is checked via an injectable TailscaleExecutableFileCheck (default statSync), so tests can fake the filesystem. Linux, Windows, and macOS without the app binary keep the previous behavior—no fallback off darwin or when the app executable is missing.

Three tests cover successful fallback, missing app binary, and no fallback on non-macOS.

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

Note

Fall back to the macOS Tailscale.app binary when tailscale is not on PATH

  • When spawning tailscale fails with a NotFound error on macOS, readTailscaleStatus and runTailscaleCommand now retry using /Applications/Tailscale.app/Contents/MacOS/Tailscale if that file exists.
  • A new TailscaleExecutableFileCheck Effect context reference wraps the file-existence check, allowing tests to inject a mock without touching the filesystem.
  • If the fallback binary does not exist, or the platform is not macOS, the original NotFound error is preserved and surfaced as a TailscaleCommandSpawnError.

Macroscope summarized e17cca1.

…t on PATH

Tailscale's macOS app doesn't add `tailscale` to PATH; users typically
work around this with a shell alias to the app's bundled binary. That
alias is invisible to our direct (non-shell) subprocess spawn, so
`readTailscaleStatus`/`ensureTailscaleServe`/`disableTailscaleServe`
fail with ENOENT and Tailnet detection silently reports "not on
Tailscale" even when Tailscale is running.

When spawning `tailscale` fails with ENOENT on darwin, retry once
against the app's bundled CLI at
/Applications/Tailscale.app/Contents/MacOS/Tailscale if that file
exists. No change in behavior on other platforms or when the app
binary isn't present.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

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 Plus

Run ID: 3496d7bb-314a-457e-8298-39f80d130286

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

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.

@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 Jul 31, 2026
@macroscopeapp

macroscopeapp Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved e17cca1

Straightforward macOS bug fix that adds a fallback to the Tailscale.app binary when tailscale isn't on PATH. The change is defensive (only triggers on NotFound, checks file existence), platform-scoped to darwin only, and includes comprehensive test coverage for all scenarios.

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.

1 participant