Skip to content

Platform behaviour — what the default opener actually does

Once a URL clears the gate, the default opener hands it to github.com/cli/browser, which picks a mechanism per operating system. What it picks changes what "opening a URL" costs, what it can fail with, and what appears in your process's output.

This page describes cli/browser v1.3.0, the version pinned in go.mod. If that pin moves, re-check this page against the release.

What runs on each operating system

GOOS Mechanism
darwin open <url>
linux The first of xdg-open, x-www-browser, www-browser, wslview found on PATH, in that order
windows The ShellExecute Windows API call — not a subprocess
freebsd, openbsd xdg-open <url>, with install xdg-utils from ports(8) appended if it is missing
netbsd xdg-open <url>, with install xdg-utils from pkgsrc(7) suggested if it is missing
anything else Fails immediately with openBrowser: unsupported operating system: <goos>

On Linux the wslview entry is what makes this work inside WSL: it hands the URL to the Windows host's default browser when no native Linux opener is installed.

Is a shell involved?

No. On the Unix-like platforms the URL is passed as a single argv element to exec.Command, so shell metacharacters in the URL — ;, |, backticks, $() — are inert. There is no sh -c anywhere in the path.

On Windows there is no command line at all: the URL goes to ShellExecute as a UTF-16 string. Note that this contradicts a common description of Windows URL opening as a rundll32 url.dll,FileProtocolHandler invocation — that is not what this dependency does at v1.3.0.

Does OpenURL block until the browser closes?

It blocks until the launcher exits, not until the browser closes. cli/browser uses cmd.Run(), which waits for the spawned process to finish.

In practice xdg-open and open hand the URL to an already-running browser and return within a few hundred milliseconds. The cases where that is not true are worth knowing:

  • If no browser is running, the launcher may wait for the browser to start.
  • Some xdg-open implementations do not return until the handler they invoked returns.
  • On Windows, ShellExecute returns as soon as the shell has accepted the request.

Treat OpenURL as a call that usually returns quickly but has no guaranteed upper bound. If you are on a hot path, call it from a goroutine. The context you pass will not help: it is checked before the open and has no effect once the opener is running.

Where does the launcher's output go?

To your process's standard output and standard error. cli/browser sets the spawned command's Stdout and Stderr to package-level variables that default to os.Stdout and os.Stderr.

This matters for a CLI that writes machine-readable output: a chatty xdg-open can interleave a warning into your JSON. The variables live in cli/browser, not in this module, and this module does not expose a way to redirect them. If you need the output silenced, supply your own Opener that runs the launcher with the streams you want.

On Windows, ShellExecute produces no such output.

What happens on a headless machine or in CI?

The URL passes validation, the opener runs, and it fails. On Linux with none of the four providers installed you get:

invoking URL opener: exec: "xdg-open,x-www-browser,www-browser,wslview": executable file not found in $PATH

A container with xdg-utils installed but no display is worse than one without: the launcher exists, runs, and fails or hangs depending on the implementation.

For tests, do not rely on either behaviour — inject a fake opener instead, so nothing platform-specific runs at all. See Test without launching a browser.

Which browser gets used?

Whichever the operating system has registered as the default handler for the scheme. Neither this module nor cli/browser chooses a browser, reads $BROWSER directly, or offers a way to request one. xdg-open consults the desktop's own configuration, which on many systems does honour $BROWSER; that behaviour is the launcher's, not this module's, and it varies by distribution.

The same applies to mailto: — the mail client that opens is the OS default, and there is no way to target a different one from here.

Which platforms are actually tested?

None of them, in this repository. The unit tests never invoke the default opener: every test injects a fake, so the suite proves the validation gate and the option plumbing, not the platform handlers.

That means the per-OS behaviour on this page comes from the dependency's source, not from a test run in this repository. It is accurate for v1.3.0 and it is not regression-guarded here — a cli/browser bump could change it without failing a test.

See also