CLI tool
sindresorhus/open avatar
sindresorhus/open

sindresorhus/open: Opening URLs, Files and Apps from Node.js

Open stuff like URLs, files, executables. Cross-platform.

3,514 stars257 forksJavaScriptMIT

At a glance

What is it?
A cross-platform Node.js package that launches URLs, files and executables through the platform's own opener. It is small, ESM-only, and deliberately makes no security guarantees about untrusted input.
Who is it for?
Adopt sindresorhus/open if you are writing a Node.js CLI or script that needs to hand a URL, file or executable to the operating system's default handler, and you can accept ESM and Node 20 or later. Do not adopt it for browser code, for Electron (the README points to shell.openPath instead), or if you are passing untrusted input and cannot sanitize it, since the package states it makes no security guarantees.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 16 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What sindresorhus/open actually does, and who it is for

The package answers one question: given a URL, a file path or an executable name, how do I ask the operating system to open it with whatever the user has configured as the default handler? The README states the intent plainly: it is meant to be used in command-line tools and scripts, not in the browser. That sentence is the whole scope statement. If you are building a CLI that prints a report and then wants to show it, or a build tool that opens a generated HTML file, or a script that launches the user's editor on a config file, this is the layer you would call.

The repository is a single JavaScript entry point (index.js) plus a TypeScript declaration file (index.d.ts), a bundled xdg-open script, and a license file. The package.json lists only those three files in its files array, so nothing else ships to npm. There is no daemon, no background service, no configuration file. The package's own keywords list reads like a description of the problem space: opener, launch, xdg-open, browser, editor, executable, url, spawn.

It is not a general-purpose process manager. It does not keep track of what it launched, does not manage windows, and does not abstract over application state. The API surface is two functions and one object: open, openApp, and apps.

How the platform dispatch works under the hood

The README gives the mechanism in one line: it uses the command open on macOS, start on Windows and xdg-open on other platforms. That is the entire abstraction. The package detects the platform and spawns the appropriate system command with the target as an argument.

The README also states that it uses spawn instead of exec, and lists that as a safety property. The distinction matters: exec runs a shell and interpolates a command string, while spawn takes an executable and an argument array. That does not make the package safe for untrusted input, and the README says so directly: this package does not make any security guarantees, and if you pass in untrusted input, it is up to you to properly sanitize it.

On Linux the package ships its own copy of the xdg-open script, which the README describes as the latest version from the xdg-utils repository. That means the behavior on Linux is pinned to whatever script version is bundled rather than to the xdg-open installed on the user's machine. That is a deliberate trade-off: more predictable behavior across distributions, at the cost of diverging from a system xdg-open that a distribution may have patched.

For WSL, the README says the package automatically uses Windows integration through PowerShell when available, and falls back to xdg-open if PowerShell is inaccessible, for example in sandboxed environments. The dependencies list reflects this: wsl-utils, powershell-utils, is-in-ssh and is-inside-container are all there to decide which path to take. The README also notes that on WSL you can pass the app's full path, giving the example of /mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe for a Windows installation of Chrome.

Installing sindresorhus/open and opening your first target

Installation is a single npm command. The README gives it as:

bash
npm install open

Before you run it, read the warning that sits directly below in the README: the package is native ESM and no longer provides a CommonJS export. If your project is CommonJS, you have to convert to ESM or use the dynamic import() function. The README explicitly asks people not to open issues about CommonJS and ESM. The package.json confirms this with "type": "module" and an exports map that points default at ./index.js.

The engines field in package.json requires Node 20 or later, so check your runtime before installing.

The README's usage example imports the default export along with openApp and apps, then awaits open on a file path with the wait option set to true, and logs a message when the viewer quits. The same block shows opening a URL in the default browser, opening it in a named browser, passing app arguments, and opening an app by name. The pattern is always the same: call the function, await the promise.

What you get back is a promise for the spawned child process. The README says you would normally not need to use that return value, but it can be useful if you want to attach custom event listeners or operate on the process directly. So the minimal call is just an await, and the advanced call is the same await with the returned process held in a variable.

One detail worth catching early: the app name is platform dependent. The README warns against hard coding it in reusable modules and gives Chrome as the example, where the name is google chrome on macOS, google-chrome on Linux and chrome on Windows. The apps object exists to work around exactly that, and the README recommends using it when possible. The apps object supports chrome, firefox, edge and brave, plus browser and browserPrivate, which resolve through the default-browser package. The README notes that browser and browserPrivate only support those four browsers.

The wait option and where it stops behaving as expected

The wait option defaults to false, and when false the promise is fulfilled immediately when the app opens. When true, the README says the promise waits for the opened app to exit before fulfilling, and adds a clarification that is easy to skim past: it waits for the app to exit, not just for the window to close. That distinction changes what your script does. If a user closes a document window but leaves the application running, your await has not resolved.

On Windows the README states you have to explicitly specify an app for wait to be able to work at all. That is a real constraint on a cross-platform code path: the same call that waits on macOS may not wait on Windows unless an app is named.

The bigger failure mode is documented in a warning block. When opening URLs in browsers while the browser is already running, wait will not work as expected, because browsers use a single-instance architecture where new URLs are passed to the existing process, causing the command to exit immediately. The README suggests using the newInstance option on macOS to force a new browser instance, or avoiding wait with browsers altogether. This is the kind of limitation that is better read before you design around it than discovered in a bug report.

There is a related option, allowNonzeroExitCode, which defaults to false and permits the opened app to exit with a nonzero code when wait is true. The README says plainly: we do not recommend setting this option, and that the convention for success is exit code zero. Treat that as a signal that the option exists for edge cases the maintainer would rather you not normalize.

Where sindresorhus/open is the wrong tool

Three cases stand out from the documentation itself.

Browser code. The README's first scoping sentence rules it out: this is meant to be used in command-line tools and scripts, not in the browser. There is no browser build in the files array and no browser export in the exports map.

Electron. The README points elsewhere: if you need this for Electron, use shell.openPath() instead. That is a direct instruction, not a hint, and it means Electron applications should not add this dependency to do the same job.

Untrusted input. The package states it makes no security guarantees and puts sanitization on the caller. Using spawn rather than exec reduces one class of shell interpolation risk, but the README does not claim it eliminates the problem, and the project's own framing should be taken at face value. If your target string comes from a remote source, a user-supplied field, or a URL parameter, this package is not the place to solve that.

There is also a quieter mismatch: if you need to know whether the user actually did anything with what you opened, this package cannot tell you in the general case. It can tell you that the platform command ran. The wait option narrows that gap only on platforms and application types where it works, and the browser caveat above shows how narrow that can be.

How it differs from calling the platform command yourself

The obvious alternative is to spawn open, start or xdg-open directly from your own code, or to shell out with child_process. That is genuinely viable, and for a single-target script on a single platform it may be less machinery than a dependency.

The difference in approach is that sindresorhus/open carries the platform matrix for you. It picks the command, bundles a specific xdg-open script for Linux rather than relying on the one installed, handles WSL by preferring PowerShell and falling back to xdg-open, and provides the apps object so you do not hard code google chrome versus google-chrome versus chrome. It also exposes a typed API through index.d.ts, and the repository runs tsd as part of its test script, so the types are checked rather than decorative.

If you write the platform dispatch yourself, you own all of that: the WSL detection, the app-name differences, the bundled xdg-open version, the ESM packaging. That is the trade. The dependency is small (six runtime dependencies, all from the same author's ecosystem), but it is still a dependency, and it brings the ESM-only constraint into your build.

A second alternative is the older node-open lineage. The README positions this package as the successor that fixes most of the original node-open issues, and lists the specific differences: app arguments, spawn instead of exec, the bundled xdg-open script, and WSL path support. If you are maintaining code that still depends on the older package, those four items are the concrete reasons to move.

Maintenance, licence and upgrade considerations

The repository is not archived, and the last push was on 2026-09-14. Releases have been frequent and recent: v11.0.4 on 2026-09-14, v11.0.3 on 2026-09-11, and v11.0.2 on 2026-08-29. The README's own rationale list opens with the claim that the package is actively maintained, and the push and release dates are consistent with that.

The licence is MIT, declared in package.json and present as a license file at the repository root. MIT is permissive: it allows use, modification and redistribution provided the copyright notice and permission notice are retained. That is a general description of the licence text, not legal advice, and anyone embedding the package in a distributed product should read the license file and their own counsel's guidance rather than this summary.

The upgrade cost is dominated by the ESM constraint. The README states the package no longer provides a CommonJS export and that CommonJS consumers must convert to ESM or use dynamic import(). That is a migration cost that lands once, at the point you adopt or upgrade across the boundary, and it is the single most likely reason a team delays moving to the current major version. The engines field requiring Node 20 or later is a second, simpler gate.

Beyond those two, the surface is small enough that upgrades are mostly about the bundled xdg-open script and the app-name detection. The README notes that the app name is platform dependent and should not be hard coded, which is also the practical advice for upgrades: if you used the apps object instead of literal names, platform-level changes are less likely to reach your code.

Editorial conclusion

Adopt sindresorhus/open if you are writing a Node.js CLI or script that needs to hand a URL, file or executable to the operating system's default handler, and you can accept ESM and Node 20 or later. Do not adopt it for browser code, for Electron (the README points to shell.openPath instead), or if you are passing untrusted input and cannot sanitize it, since the package states it makes no security guarantees. Before you commit, verify three things in your own environment: that your project can consume an ESM-only export or use dynamic import(), that you are on Node 20 or later per the engines field, and whether the wait option behaves as you need on the platform you target, given the README's warning that browsers use a single-instance architecture and the command exits immediately.

Frequently asked questions

Does sindresorhus/open work in the browser?

No. The README states the package is meant to be used in command-line tools and scripts, not in the browser, and the package's exports map only provides the Node entry point.

Why can't I use sindresorhus/open with require() in CommonJS?

Because the package is native ESM and no longer provides a CommonJS export. The README says CommonJS projects must convert to ESM or use the dynamic import() function, and asks people not to open issues about it.

Why does the wait option not work when opening a URL in a browser with sindresorhus/open?

The README warns that browsers use a single-instance architecture where new URLs are passed to the existing process, so the command exits immediately. It suggests using the newInstance option on macOS to force a new browser instance, or avoiding wait with browsers.

Which Node.js version does sindresorhus/open require?

The engines field in package.json requires Node 20 or later.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. sindresorhus/open on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sindresorhus-open.svg)](https://hysenlabs.com/projects/sindresorhus-open)