The opencode pair QR encodes raw JSON, so a phone camera cannot open it; after this it is an app.opencode.ai link that a camera opens in the mobile app when installed and in the web app otherwise.
The payload sits in the URL fragment, not the query, so the server password never reaches app.opencode.ai or its logs. The hosted page never redirects on its own: for HTTP LAN or loopback servers it shows a screen and lets the user choose to open the server's own web UI.
Where the browser ends up depends on which server addresses the hosted page can actually use:
ConnectRoute (/connect)
pairing = decode(location.search) ?? decode(location.hash)
- servers.add(pairing.urls[0]); navigate("/")+ url = urls.find(== location.origin)+ ?? urls.find(page is not https || url is https)+ if url+ servers.add(url); navigate("/")+ else+ <ConnectLocalScreen urls>+ LAN address → "Open on local network" → http://<lan>/connect#<same payload>+ loopback only → "Open on this computer" + `opencode service set hostname 0.0.0.0`
From a phone camera:
sequenceDiagram
participant Camera
participant iOS
participant Hosted as app.opencode.ai
participant Server as http://lan:49374
Camera->>iOS: open link
alt app installed
iOS->>iOS: OpenCode app decodes fragment, connects
else no app
iOS->>Hosted: GET /connect (fragment stays client-side)
Hosted-->>iOS: local network screen
iOS->>Server: GET /connect#payload (user tap)
Server-->>iOS: served web UI, paired
end
Loading
Files:
packages/cli/src/commands/handlers/pair.ts # QR + "Link" line use the hosted fragment URL
packages/app/src/servers/connect/
├── pairing.ts # + decodePairingScan: JSON | ?data= | #fragment
├── scanner.tsx # uses decodePairingScan
├── browser.ts # isLoopback extracted from isMixedContent
+└── local.tsx # ConnectLocalScreen (LAN / loopback variants)
packages/app/src/shell/routes/routes.tsx # ConnectRoute picks a usable URL or shows the screen
packages/app/src/runtime/i18n/en.ts # 7 keys for the new screen
packages/app/e2e/regression/pairing.spec.ts # hosted https page + LAN-only payload → screen → hop
services/www/src/docs/content/cli/{web,commands}.mdx
The e2e case intercepts https://app.opencode.ai/** and serves the dev build through it, so the page runs under a real https origin and mixed-content rules apply without certificates.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #49291 | Public report | iOS reference branch
Why the change
The opencode pair QR encodes raw JSON, so a phone camera cannot open it; after this it is an app.opencode.ai link that a camera opens in the mobile app when installed and in the web app otherwise.
Special things to note
Change outline
The one link every emitter prints and every scanner accepts:
https://app.opencode.ai/connect#<base64url({ urls: [...], username: "opencode", password })>Where the browser ends up depends on which server addresses the hosted page can actually use:
From a phone camera:
sequenceDiagram participant Camera participant iOS participant Hosted as app.opencode.ai participant Server as http://lan:49374 Camera->>iOS: open link alt app installed iOS->>iOS: OpenCode app decodes fragment, connects else no app iOS->>Hosted: GET /connect (fragment stays client-side) Hosted-->>iOS: local network screen iOS->>Server: GET /connect#payload (user tap) Server-->>iOS: served web UI, paired endFiles:
packages/cli/src/commands/handlers/pair.ts # QR + "Link" line use the hosted fragment URL packages/app/src/servers/connect/ ├── pairing.ts # + decodePairingScan: JSON | ?data= | #fragment ├── scanner.tsx # uses decodePairingScan ├── browser.ts # isLoopback extracted from isMixedContent +└── local.tsx # ConnectLocalScreen (LAN / loopback variants) packages/app/src/shell/routes/routes.tsx # ConnectRoute picks a usable URL or shows the screen packages/app/src/runtime/i18n/en.ts # 7 keys for the new screen packages/app/e2e/regression/pairing.spec.ts # hosted https page + LAN-only payload → screen → hop services/www/src/docs/content/cli/{web,commands}.mdxThe e2e case intercepts https://app.opencode.ai/** and serves the dev build through it, so the page runs under a real https origin and mixed-content rules apply without certificates.