Self-hosted service
gildas-lormeau/single-file-cli avatar
gildas-lormeau/single-file-cli

SingleFile CLI drives a real browser and inherits its limitations

CLI tool for saving a faithful copy of a complete web page in a single HTML file (based on SingleFile)

1,668 stars147 forksJavaScriptAGPL-3.0

At a glance

What is it?
gildas-lormeau/single-file-cli saves a complete web page as one HTML file by injecting the archiver into a headless browser over the Chrome DevTools Protocol, or WebDriver BiDi with Firefox. Running a browser is the point: it is why the output is faithful, and why Firefox support comes with documented limits and why containers need a volume to get files out.
Who is it for?
Use SingleFile CLI when you need to save a complete web page as one HTML file and are willing to run a browser per capture. Do not use it as a general scraper for structured data, since the output is a rendered HTML document rather than parsed records, and do not ignore the AGPL-3.0 licence if you intend to build a service on it, because the README points licensing questions for commercial use at the author directly.
Can I use it commercially?
Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
Is it still maintained?
Yes. The repository last received commits 4 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 October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

It injects into a headless browser, it does not fetch HTML

The architecture is stated in the introduction and it is the thing to understand first. SingleFile is launched by running it into a headless browser. It runs through Deno as a standalone script injected into the web page via the Chrome DevTools Protocol, or via WebDriver BiDi when using Firefox, instead of being embedded into a WebExtension.

That last clause matters historically: the browser extension is the original distribution, and this CLI is the same code path without the extension, which is what makes it scriptable and usable in a container.

The consequence is that a capture is a rendered page. Scripts have run, images have loaded, the DOM is what the user would have seen. That is exactly what makes the output durable, and exactly why it is slower and heavier than fetching bytes over HTTP. A browser-driven capture can differ from one based only on an HTTP response.

The browser is also the dependency that decides where this runs well and where it runs badly. The README expects Chrome or a Chromium-based browser in the default folder, and says installations for the current user are also found: `%LOCALAPPDATA%` on Windows, `~/Applications` on macOS, and snap and the `PATH` on Linux. If yours is elsewhere, `--browser-executable-path` tells the tool where to look.

Firefox is supported through `--browser-engine firefox`, using WebDriver BiDi rather than the DevTools Protocol, and the README says there are limitations described in the help rather than pretending they do not exist. If you need Firefox on Flatpak specifically, it needs access to `/tmp` because that is where the temporary profile is created, and the README gives the override command `flatpak override --user --filesystem=/tmp org.mozilla.firefox`.

`--browser-executable-path` must then point at the Flatpak export path, either `/var/lib/flatpak/exports/bin/org.mozilla.firefox` or the per-user equivalent under `~/.local/share/flatpak`.

Three runtimes and four ways to install

Installation is unusually well covered because the tool is meant to fit into whatever you already have.

The shortest path is to download the executable from the releases page and put it wherever you like. No install step at all.

The Docker path pulls the prebuilt image `capsulecode/singlefile` and then tags it locally as `singlefile`, which is why every run command below uses the short name. There is also a manual build, which clones recursively:

sh
git clone --depth 1 --recursive https://github.com/gildas-lormeau/single-file-cli.git
cd single-file-cli

and then builds the image locally:

sh
docker build --no-cache -t singlefile .

For npm users there is a package. Installing it and running it through `npx` avoids a global install entirely, and `-g` is available if you prefer one:

sh
npm install "single-file-cli"
npx single-file ...

The manual routes are unzipping the main archive from GitHub, or a shallow recursive clone, followed by making the `single-file` script executable with `chmod +x single-file` on Linux, Unix and BSD.

The runtime picture explains why there are so many entries in the package manifest. It declares engines for Deno 2.2 or later, Bun 1.2 or later, and Node 24 or later, and it is an ES module. The launchers in the repository include `single-file`, `single-file.bat`, `single-file-node.js` and `single-file-node.bat`, and the `bin` entry maps the `single-file` command to `single-file-node.js`. Deno is what the introduction names as the runtime the script runs through, and the rest are supported paths to the same code.

One dependency is declared, `simple-cdp`. There is a `test:dev` script that sets `SINGLE_FILE_TARGET=dev` and a `test:firefox` script that sets `SINGLE_FILE_BROWSER_ENGINE=firefox`, so both the development core and the Firefox path have dedicated test runs rather than being an afterthought.

Containers need somewhere to put the file

The Docker usage section is where the container nature of the tool becomes concrete, and there is one detail worth understanding before you run it.

The image installs Chromium and Firefox plus a base font and an emoji font, runs as the `node` user, and its entrypoint already carries options: `--browser-single-process=false`, an output directory of `./out/`, and `--dump-content`.

That last option is why the obvious invocation, `docker run singlefile "https://www.wikipedia.org"`, writes to your terminal instead of a file. If you want the file on your host, either redirect the standard output, which works because the content is dumped: `docker run singlefile "https://www.wikipedia.org" > wikipedia.html`.

Or mount a volume at `/usr/src/app/out` and name the output file so it lands there: `docker run -v $(pwd):/usr/src/app/out singlefile "https://www.wikipedia.org" wikipedia.html` on Linux and Unix, with `%cd%` in place of `$(pwd)` on Windows. For batch work the README suggests turning dumping off and using the filename template, passing `--dump-content=false` and letting `--filename-template` decide the names.

An alternative Dockerfile exists in a separate project, SingleFile-dockerized, which adds an HTTP server on top so you can submit URLs over the network instead of the command line.

The image pins the version at build time through a `VERSION` build argument, defaulting to latest, installed with `npm install --omit=dev single-file-cli@${VERSION}`. Pin it explicitly if you are running this in anything scheduled, because the browser and the archiver core both move and a container that changes behaviour on a schedule is a bad time machine.

Crawling, rewrite rules and the self-extracting archive

Single page capture is the core, and everything else builds on it.

The syntax is `single-file <url> [output] [options...]`, and `single-file --help` lists the rest. The simplest operations are dumping content to the console with `--dump-content`, or naming an output file directly.

Batch input comes from a list file with `--urls-file=list-urls.txt`.

Crawling has more knobs than it first appears. `--crawl-links=true` enables it, `--crawl-inner-links-only` decides whether the crawl stays on the site, `--crawl-max-depth` bounds it, and `--crawl-external-links-max-depth` bounds the off-site variant separately. Rewrite rules let crawls normalize URLs by removing query strings. The documented example strips the query string with the rewrite rule `^(.*)\?.*$ $1` passed to `--crawl-rewrite-rule`.

Another example restricts the crawl to URLs matching the pattern `^.*wikipedia.*Single page capture is the core, and everything else builds on it.

The syntax is `single-file <url> [output] [options...]`, and `single-file --help` lists the rest. The simplest operations are dumping content to the console with `--dump-content`, or naming an output file directly.

Batch input comes from a list file with `--urls-file=list-urls.txt`.

Crawling has more knobs than it first appears. `--crawl-links=true` enables it, `--crawl-inner-links-only` decides whether the crawl stays on the site, `--crawl-max-depth` bounds it, and `--crawl-external-links-max-depth` bounds the off-site variant separately. Rewrite rules let crawls normalize URLs by removing query strings. The documented example strips the query string with the rewrite rule `^(.*)\?.*$ $1` passed to `--crawl-rewrite-rule`.

with `--crawl-inner-links-only=false` and `--crawl-external-links-max-depth=1`.

Several options turn a crawl into an archive rather than a folder of files. `--crawl-save-archive=true` produces a single self-extracting ZIP, `--crawl-save-archive-mark-unarchived-links=true` marks the links to pages that were not captured, and `--compress-content=true` compresses the content. The documented example archives the first website on the internet, `http://info.cern.ch`, to depth 2, which is the sort of target that exposes performance problems early.

Logging in to a page is handled with browser profiles. You create one against a login URL, log in in the browser window that opens, quit the browser to persist the profile, and then reuse it. The README is explicit that the profile is copied and left unmodified, which is the right behaviour for a tool you run repeatedly:

sh
single-file --create-browser-profile ./profiles/example https://www.example.com/login
single-file https://www.example.com/account account.html --browser-profile=./profiles/example

Exit codes and manifests are how you trust an archive

Two features make this usable for anything you might need to verify later, and both are easy to overlook.

The first is the exit code contract. It is 0 when all pages have been saved, 1 when at least one capture failed while the rest of the batch or crawl were still saved, and 255 when an error prevented the process from running at all.

That middle case is the one that matters. A crawl with any failed capture exits 1, not 0, while the other saved pages or crawl results are retained. If you script this in cron and only check for success, you will silently accumulate incomplete archives. The distinction between 1 and 255 also separates a partial capture from a total failure, which is the difference between retrying one URL and fixing your environment.

The second is the manifest, documented in MANIFEST.md. Passing `--manifest` records what was observed while saving, for later verification:

sh
single-file https://www.wikipedia.org wikipedia.html --manifest

This is the feature that turns a capture into evidence. Without it you have a file; with it you have a record of what the tool saw. This distinction is useful when someone later needs to verify what the tool observed while saving.

The licensing is AGPL, with code derived from third-party projects under MIT, and the README asks interested parties to contact the author directly about licensing SingleFile for a commercial service or product. That last line is a signal: AGPL has network use provisions, so wrapping this in a service is exactly the case the author has anticipated and reserved.

Maintenance is brisk. v2.18.1, v2.18.2 and v2.18.3 all shipped in October 2026, the last on 2026-10-13, and the repository is not archived. `update-core.sh` exists to pull a new archiver core, and `./compile.sh` compiles executables into `/dist` for anyone who wants a native binary built from source.

Editorial conclusion

Use SingleFile CLI when you need to save a complete web page as one HTML file and are willing to run a browser per capture. Do not use it as a general scraper for structured data, since the output is a rendered HTML document rather than parsed records, and do not ignore the AGPL-3.0 licence if you intend to build a service on it, because the README points licensing questions for commercial use at the author directly. Verify first that a Chrome or Chromium build is where the tool expects it, or set `--browser-executable-path`, that you check the exit code, since 1 means a partial capture and only 0 means every page was saved, and that you read the manifest output from `--manifest` if the capture has to be verifiable later.

Frequently asked questions

How do I install SingleFile CLI?

Download the executable from the releases page, install the npm package with `npm install "single-file-cli"` and run it through `npx`, unzip the main archive, or make a shallow recursive git clone and run `chmod +x single-file`. Docker is also supported with `docker pull capsulecode/singlefile`.

Why does SingleFile need a browser?

It is injected into a headless browser and saves the rendered page rather than the raw response. It talks to Chrome or Chromium over the Chrome DevTools Protocol, and to Firefox over WebDriver BiDi with `--browser-engine firefox`, which the README notes comes with limitations detailed in the help.

How do I get the saved file out of the Docker container?

Either redirect the output, for example `docker run singlefile "https://www.wikipedia.org" > wikipedia.html`, or mount your host directory at `/usr/src/app/out` and pass an output filename, for example `docker run -v $(pwd):/usr/src/app/out singlefile "https://www.wikipedia.org" wikipedia.html` on Linux and Unix.

What do the SingleFile CLI exit codes mean?

0 means all pages were saved, 1 means at least one capture failed while the rest were still saved, and 255 means an error prevented the process from running. A crawl that partly fails exits 1, so checking only for success will hide incomplete archives.

Can SingleFile save a page that requires logging in?

Yes, with a browser profile. Create one with `--create-browser-profile` pointed at the login URL, log in in the browser window, quit the browser to save the profile, then pass `--browser-profile` on later runs. The profile is copied and left unmodified.

What is the --manifest option for?

It records what was observed while saving, for later verification, and is documented in MANIFEST.md. Use `single-file <url> <output> --manifest` when the saved page needs to be checkable later rather than merely readable.

Official sources

  1. gildas-lormeau/single-file-cli on GitHub
  2. License: AGPL-3.0
  3. Project website
  4. README
  5. Releases
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/gildas-lormeau-single-file-cli.svg)](https://hysenlabs.com/projects/gildas-lormeau-single-file-cli)

Was this useful?