Skip to content

tui: /pair QR code encodes the loopback 127.0.0.1 link, so other devices cannot connect #58810

Open

Description

@pedrosamanuel

Description

The QR code shown by /pair in the TUI encodes the loopback link, so no other device can connect.

/pair renders links[0], i.e. the first entry of server.info().urls, which is the loopback address (127.0.0.1). The same machine's opencode pair CLI does filter loopback addresses out of the QR payload, so the two entry points behave differently.

The dialog should encode the reachable (non-loopback) addresses, or omit the QR when only loopback is available, like the CLI does.

Related: #54051 (open PR fixing this in packages/tui/src/component/dialog-pair.tsx).

Separately, the CLI QR encodes {"code","urls"} JSON, which generic phone cameras render as text instead of an openable link — tracked in #54871.

Plugins

None configured (only a @opencode-ai/plugin dependency in ~/.config/opencode/package.json).

OpenCode version

v2.1.4 (latest channel)

Steps to reproduce

  1. Run opencode and open /pair in the TUI.
  2. Scan the QR code with a phone (or decode it with a standard QR decoder).
  3. The payload is http://127.0.0.1:<port>/auth/connect/<code>, which is unreachable from the phone.

Expected behavior

The QR encodes an address reachable from other devices on the LAN (or the remote tunnel URL when remote access is enabled), and 127.0.0.1 is not what gets scanned.

Actual behavior

QR decodes to http://127.0.0.1:<port>/auth/connect/<code> even when the server is reachable at 192.168.x.x:<port> and the CLI lists those URLs.

Operating System

Windows 11 Pro 10.0.26300

Terminal

VS Code integrated terminal (TERM_PROGRAM=vscode), shell: cmd.exe

Additional context

  • opencode pair on the same machine prints http://127.0.0.1:..., http://192.168.1.146:..., etc., and its QR only carries the non-loopback URLs.
  • Workaround: use the LAN link printed by opencode pair manually.
  • Source: packages/tui/src/component/dialog-pair.tsx renders renderUnicodeCompact(url(), { border: 1 }) with url = value.links[0].

Activity

  1. opencode-agent commented on Oct 24, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this on v2.1.4, and it still happens on the latest dev (13a615b).

    Steps:

    1. Bind the service to 0.0.0.0. server.info().urls then lists http://127.0.0.1:<port> first, followed by the LAN address.
    2. Open /pair in the TUI.
    3. Decode the QR code. It decodes to http://127.0.0.1:<port>/auth/connect/<code>, even though the LAN link is listed right next to it.

    The dialog also still shows the "Run opencode service set hostname 0.0.0.0" hint when the service is already bound to 0.0.0.0.

    The cause is that dialog-pair.tsx builds the QR from the first link, while opencode pair filters out loopback addresses first. Open PR #54051 addresses this. #54871 is the separate issue about the CLI QR containing JSON instead of a plain URL.

    The recording and screenshots below show the dialog, and the script below reproduces it. Until this is fixed, opening the LAN link from opencode pair (or from the dialog's link list) works.

    recording.mp4

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Reproduces #58810: the TUI /pair QR code encodes the loopback link.
    // The service is bound to 0.0.0.0 (via a wrapper), so server.info() lists
    // 127.0.0.1 first, then the LAN address(es).
    import { Effect } from "effect"
    import { OpenCodeDriver } from "opencode-drive"
    import { chmodSync, writeFileSync } from "node:fs"
    
    // Wrapper that runs opencode (release from PATH, or the checkout in $OC_DEV) and
    // binds the service to 0.0.0.0, as `opencode service set hostname 0.0.0.0` would.
    const wrapper = "/tmp/opencode-58810-wrap.sh"
    writeFileSync(wrapper, "#!/usr/bin/env bash\nif [ -n \"$OC_DEV\" ]; then\n  run=(bun --conditions=browser --preload=\"$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js\" \"$OC_DEV/packages/cli/src/index.ts\")\nelse\n  run=(opencode)\nfi\nif [ \"$1\" = \"serve\" ]; then shift; exec \"${run[@]}\" serve --hostname 0.0.0.0 \"$@\"; fi\nexec \"${run[@]}\" \"$@\"\n")
    chmodSync(wrapper, 0o755)
    
    export default OpenCodeDriver.use(
      {
        opencode: { command: [wrapper] },
        tui: { recording: true, keypressOverlay: true, viewport: { cols: 140, rows: 50 } },
      },
      ({ ui, opencode }) =>
        Effect.gen(function* () {
          const info: any = yield* opencode.server.info()
          yield* Effect.log(`server.info().urls = ${JSON.stringify(info.urls)}`)
          yield* ui.screenshot("home")
          yield* ui.type("/pair")
          yield* Effect.sleep(500)
          yield* ui.enter()
          yield* Effect.sleep(1500)
          yield* ui.screenshot("after-enter")
          yield* ui.waitFor("Links", { timeout: 15000 } as any)
          yield* Effect.sleep(1500)
          const frame: any = yield* ui.capture()
          yield* Effect.log(`screen:\n${frame.lines.map((l: any) => typeof l === "string" ? l : (l.text ?? JSON.stringify(l))).join("\n")}`)
          yield* ui.screenshot("pair-dialog")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions