Skip to content

fix(open-url): flag when a web URL may open the browser instead of an app#476

Draft
latekvo wants to merge 1 commit into
mainfrom
fix/open-url-ios-deeplink-note
Draft

fix(open-url): flag when a web URL may open the browser instead of an app#476
latekvo wants to merge 1 commit into
mainfrom
fix/open-url-ios-deeplink-note

Conversation

@latekvo

@latekvo latekvo commented Jul 6, 2026

Copy link
Copy Markdown
Member

Problem

An agent calling open-url to deep-link into an app gets Safari instead:

open-url { "url": "https://bsky.app/profile/tvpworld.bsky.social" }  →  opens Safari

Root cause (not a wrapper bug — an iOS Simulator limitation)

open-url on iOS is a thin wrapper over xcrun simctl openurl. An https://
URL is a Universal Link, which only reaches a native app when that app is
installed and verified for the link's domain. Two things bite here:

  1. On the iOS Simulator, simctl openurl routes Universal Links to Safari
    even when the owning app is installed — a long-standing, documented
    limitation. Custom URL schemes (scheme://…) route reliably; Universal
    Links do not.
  2. In the reported case the Bluesky app isn't installed at all, so Safari is
    the only valid handler — identical to tapping the link on a real iPhone with
    Bluesky absent.

Reproduced on a booted iOS 18.5 sim: simctl openurl <bsky https link>
launches com.apple.mobilesafari; the sim has zero third-party apps installed;
bluesky://… fails with -10814 (no registered handler), confirming absence.

The real defect on our side: the result was always { opened: true }, so a
web URL that silently fell back to Safari was indistinguishable from a
successful deep-link — the caller (an agent) believed it had reached the native
app. There is no reliable way to observe which app actually handled the URL
(Safari and other apps are invisible to the native-devtools socket), so the fix
makes the ambiguity explicit rather than guessing.

Fix

  • Add an optional note to the open-url result whenever a web URL (http/
    https) is opened on a native device (iOS + iOS-remote). It states the URL
    may have opened in the browser and points to the reliable path: the app's
    custom scheme (scheme://path) or launch-app with the bundle id.
  • Update the tool description with the same deep-linking caveat.
  • Custom schemes, non-web schemes (tel:, maps://, geo:), and Chromium
    navigations are unaffected (no note). No URL rewriting — the URL is still
    handed to simctl unchanged.

Scoped to iOS deliberately: Android App Links go through am start with a
different verification/chooser path, and the reported symptom is iOS-specific.

Verification

  • New unit tests (open-url-deep-link-note.test.ts, 4 cases): the helper
    returns the caveat for http/https (case-insensitive) and undefined for
    custom/non-web schemes; the iOS handler attaches note for the bsky https
    link (URL passed to simctl unchanged) and omits it for a custom scheme.
  • End-to-end against a live sim through the real openUrlTool.execute:
    • https://bsky.app/profile/…{ opened: true, url, note }
    • maps://?q=cupertino (installed native handler) → { opened: true }, no note
    • bluesky://… (not installed) → correct FailureError (existing behavior)
  • tsc --noEmit, tsc --noEmit -p tsconfig.test.json, and prettier --check
    all pass on the changed files.

… app

open-url on iOS is a thin wrapper over `xcrun simctl openurl`. Given an
https Universal Link (e.g. https://bsky.app/profile/...), the simulator
routes it to Safari rather than deep-linking into the app: Universal Links
only reach an installed, domain-verified app, and simctl openurl is
unreliable at that even when the app IS installed. The result was always
`{ opened: true }`, so a web URL that silently fell back to Safari looked
identical to a successful deep-link and misled callers into thinking the
native app had been reached.

Add a `note` to the result (and a matching description caveat) whenever a
web URL (http/https) is opened on a native device, pointing callers at the
reliable path: the app's custom scheme (scheme://path) or launch-app with
its bundle id. Custom schemes and Chromium navigations are unaffected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant