> For the complete documentation index, see [llms.txt](https://docs.unbrowse.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.unbrowse.ai/cli.md).

# CLI reference

`unbrowse <command> [args] [--json] [--base-url URL] [--no-open] [--account NAME]`

A thin shell over the REST API (`/api/v1`, see [api.md](/api.md)). Output is JSON on stdout; messages go to stderr. The CLI matches the hosted MCP ([mcp.md](/mcp.md)): every MCP tool has a command ([Same as the MCP](#same-as-the-mcp)).

## Account

| Command                 | Route                                                                                                                            |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `login`                 | Browser sign-in: OAuth 2.1 with PKCE and a loopback redirect. The token's resource is `{origin}/api` (this REST API), not `/mcp` |
| `login --key ub_live_…` | Stores an API key (create one in the console, MCP & keys)                                                                        |
| `logout`                | Deletes the stored sign-in                                                                                                       |
| `whoami`                | `GET /me`, plus the `account` in use                                                                                             |
| `usage`                 | `GET /usage`                                                                                                                     |
| `accounts`              | The sign-ins saved on this machine (name, origin, API key or OAuth; never the secret) and the current one                        |
| `accounts use <name>`   | Makes `<name>` the account commands use; `accounts use default` goes back                                                        |

Credentials are read in this order: `UNBROWSE_API_KEY`, the stored key, the stored OAuth token (refreshed when it is about to expire). Stored at `~/.config/unbrowse/cli.json`, mode 0600.

### Several accounts

Each Unbrowse account has its own vault: saved logins, synced browser cookies and learned tools. To keep, say, a personal, a business and a work identity apart on one machine, sign each in under a name and pick one per command with `--account NAME` (or `UNBROWSE_ACCOUNT`):

```sh
unbrowse login --account work                                  # sign in once per account
unbrowse cookies sync --account work --browser Chrome --profile "Profile 2"   # that account's browser sign-ins
unbrowse run --account work 'my open invoices on stripe.com'
unbrowse accounts use work                                     # make it the default; --account still overrides
unbrowse accounts                                              # list them
```

A named account is stored at `~/.config/unbrowse/accounts/<name>/cli.json`. Names are letters, digits, `-` and `_` (up to 40); `default` is the unnamed one at `cli.json`. Order: `--account`, then `UNBROWSE_ACCOUNT`, then `accounts use`, then the default. A named account only uses its own sign-in (never a v1 key or Claude Code's); `UNBROWSE_API_KEY`, when set, still wins over every account. `logout --account NAME` signs out of that one. Its cookie sync runs as its own service (`unbrowse-cookies-<name>`), and `mcp add --account NAME` registers a server named `unbrowse-<name>` that runs on that account.

First use: a command that needs an account (`run`, `discover`, `whoami`, …), run at a terminal with no credential, starts the same browser sign-in as `login` and then carries on. Without a terminal (no TTY, `CI` set, or `--json`) it exits 3 and names `unbrowse login` and `UNBROWSE_API_KEY`.

Headless / remote (a cloud agent's VM, CI, SSH) has no browser and no loopback to catch the redirect. There, `unbrowse login` — or `unbrowse login --device`, or any `login` where there is no interactive terminal — uses the device flow (RFC 8628): it prints a short code and a URL, you open the URL on your phone or laptop and approve the code, and the CLI gets its own named login once you approve. The login is that client's alone, so you can revoke a single agent without signing the others out.

## Runs

| Command                       | Route                                               |
| ----------------------------- | --------------------------------------------------- |
| `discover <query>`            | `POST /capabilities/search`                         |
| `run <task…>`                 | `POST /runs`, then `GET /runs/:id` until it settles |
| `inspect <runId>`             | `GET /runs/:id`                                     |
| `resume <runId> field=value…` | `GET /runs/:id`, then `POST /runs/:id/responses`    |
| `cancel <runId>`              | `POST /runs/:id/cancel`                             |

`run` options: `--capability ID` (run a known capability), `--url URL`, `--set key=value` (repeatable) and `--input JSON` for inputs, `--unattended` (never pause for input), `--idempotency-key K` (one is generated otherwise), `--no-wait`, `--timeout S` (default 600).

Site requests go from this machine (your IP) by default: the run's HTTP calls to the site are handed to the CLI (`POST /runs` with `egress: "client"`, answered through `/egress/:id`), each noted on stderr. Unbrowse's own services never go through your machine. `--from-unbrowse` (or `UNBROWSE_EGRESS=server`) sends them from Unbrowse instead.

Values are typed: `adults=2` is a number, `flex=true` a boolean, `x='{"a":1}'` JSON, anything else a string. `resume` names requirements by field (`origin=CDG`) and answers on the same run.

The printed run leaves out its event log (as MCP does); `--events` keeps it. When nothing fits (`no_capability`) the next steps are printed on stderr as CLI commands: `scrape` the page, `index` the site, or a near-match `--capability`.

## Pages

| Command        | Route          |
| -------------- | -------------- |
| `scrape <url>` | `POST /scrape` |

The page's main content as markdown on stdout, so it pipes; title, final URL and HTTP status on stderr. `--format markdown,links` (any of `markdown`, `html`, `text`, `links`, `raw`), `--full` (keep navigation and footers), `--render auto|always|never`, `--country GB`, `--deadline MS`. `--json` prints the whole answer with its metadata. One verified call.

## Teaching

| Command                                    | Route                                              |
| ------------------------------------------ | -------------------------------------------------- |
| `index <url> [--focus T] [--max N]`        | `POST /index`, then `GET /index/:id` until it ends |
| `index status [jobId]`                     | `GET /index/:id`, or `GET /index` for all          |
| `learn a.har b.har [--title T] [--goal G]` | `POST /learn`                                      |
| `learned [id]`                             | `GET /learned`, `GET /learned/:id`                 |

`index` has an agent explore the site in Unbrowse's cloud browser and compile each flow it can prove into a tool. It takes minutes; progress goes to stderr, Ctrl-C leaves the job running, and `--no-wait` returns the job id at once. Done means `status: done` with `indexed` above 0; then `unbrowse site <host>` lists the tools.

Record the task twice with different inputs (a HAR export from devtools) and pass both. Doing a task in the cloud browser is an MCP flow (`unbrowse_browse_*`), not a CLI command.

## Logins

| Command                  | Route                                      |
| ------------------------ | ------------------------------------------ |
| `logins`                 | `GET /logins` — masked hints, never values |
| `logins remove <origin>` | `POST /logins/remove`                      |

The CLI never takes a password. When a run needs a login nobody saved, it opens the one-time save-login page and exits 3. See [logins-and-vault.md](/logins-and-vault.md).

## Cookies — reuse your browser's sessions

Sign in to a site in your normal browser, then hand those cookies to Unbrowse so your runs act as your signed-in self. The cookies are read locally (decrypted with your OS keychain where the browser encrypts them), sent to Unbrowse, and kept per site — sealed in your vault, so the session persists.

| Command                 | What it does                                                                                                                         |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `cookies list`          | Browsers and profiles found on this machine. `--json` for an agent to choose from.                                                   |
| `cookies sync`          | Read cookies and upload them once (`POST /cookies`).                                                                                 |
| `cookies watch`         | Keep the chosen sites in sync in the foreground: re-read every interval, upload a site when its cookies change (and at least daily). |
| `cookies daemon start`  | The same as a background service that survives logouts and reboots (systemd user unit on Linux, LaunchAgent on macOS).               |
| `cookies daemon status` | Running or not, the sites, the last sync and the last error. `--json` for an agent.                                                  |
| `cookies daemon stop`   | Stop and remove the service, its config and state.                                                                                   |

```sh
unbrowse cookies list
unbrowse cookies sync --domain github.com        # just one site
unbrowse cookies sync --browser Chrome --profile Default
unbrowse cookies sync --all                      # every profile found
```

A page that shows "Sign in to view this page" (a private claude.ai artifact, a Notion or Google Docs page) comes back as `login_required` (exit 3) with the site's fix: sign in to that site once in your browser, then `unbrowse cookies sync --domain <site>` (or `cookies daemon start --domain <site>` to keep it fresh), and ask again. `--from-here` and `--from-unbrowse` change only which IP the requests leave from; neither signs you in to a site.

Options: `--browser NAME`, `--profile NAME` (from `cookies list`), `--domain d` (one site and its subdomains), `--all` (every profile; otherwise the default profile is used). Supported: Chrome, Chromium, Arc, Brave, Edge, Opera, Vivaldi, Firefox, LibreWolf, Waterfox, on macOS, Linux and Windows. Cookies a browser encrypts with a locked keyring, or a sandboxed (Flatpak/Snap) install whose key is not reachable, are reported and skipped. Everything is read locally; only the cookies you sync leave your machine. Reads work best with the browser closed.

### Keep sites in sync continuously

```sh
unbrowse cookies daemon start --domain github.com,linkedin.com   # named sites; asks to confirm, --yes in scripts
unbrowse cookies daemon start --all-sites                        # EVERY site you are signed into (incl. banking/email): confirm carefully
unbrowse cookies daemon status
unbrowse cookies daemon stop
```

The daemon syncs the sites you name — or every site in the profile with `--all-sites` (uploaded in batches, grouped by registrable site; new sites you sign into are picked up on their own). It reads from one browser profile (`--browser`, `--profile`; else the default profile), every `--interval` minutes (default 15). A site is uploaded when its cookies change, and at least once a day so the kept session never ages out; expired cookies are never sent. `start` first runs one sync and refuses to start if it cannot read the browser or reach Unbrowse. Failures back off (doubling, up to an hour) and show in `status`; a sign-out from Unbrowse says to run `unbrowse login`. The service runs the installed CLI (`npm i -g unbrowse` first: a temporary `npx` copy is refused). Logs: `journalctl --user -u unbrowse-cookies -f` (Linux), `~/Library/Logs/unbrowse-cookies.log` (macOS). Windows: run `unbrowse cookies watch` from Task Scheduler at logon.

After `unbrowse login` at a terminal, the CLI offers this once: name the sites (or press Enter to skip). Nothing is synced unless you name a site; scripts and agents (no terminal, `--json`) are never asked.

## Public registry (no account)

| Command            | Route                                                                |
| ------------------ | -------------------------------------------------------------------- |
| `registry [query]` | `GET /sites?q=`                                                      |
| `site <host>`      | `GET /sites/:host`                                                   |
| `openapi <host>`   | `GET /sites/:host/openapi.json`: OpenAPI 3.1, one operation per tool |

## Call one site tool

| Command                     | Route                                                             |
| --------------------------- | ----------------------------------------------------------------- |
| `call <host> <tool> [JSON]` | `POST /sites/:host/call/:tool` with the tool's inputs as the body |

Inputs as a JSON argument, `--input JSON` or `--set key=value` (repeatable). `--deadline MS`, `--select PATH,…` (narrow the result), `--idempotency-key K`, `--end-user ID` (org keys). Prints the run; needs an account.

## Same as the MCP

Every tool of the hosted MCP is a command. Where the REST API has a route the command uses it; the rest (the cloud browser, `map`, `sites`, `forget`, `connect`, `credits`, credentials requests and replay) are one MCP `tools/call` each, through the same proxy as `unbrowse mcp`, with the same arguments an agent sends. Required inputs are positional, in the order shown; every other input is a `--flag` (`--text-offset` is `textOffset`), typed like `run`'s `--set`. Output is the tool's result as JSON (or its text); exit 1 when the tool reports an error, 2 when a run is `input_required`. Needs an account. `tests/mcp-parity.test.ts` fails when an exported MCP tool has no command.

| MCP tool                                                         | Command                                                                         |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `unbrowse_discover`, `search`                                    | `discover <query>`                                                              |
| `unbrowse_run`, `_inspect`, `_resume`, `_cancel`                 | `run`, `inspect`, `resume`, `cancel`                                            |
| `unbrowse_scrape`, `fetch`                                       | `scrape <url>`                                                                  |
| `unbrowse_index`, `_index_status`                                | `index <url>`, `index status`                                                   |
| `unbrowse_learn`                                                 | `learn <a.har> <b.har>`                                                         |
| `unbrowse_usage`                                                 | `usage`                                                                         |
| `unbrowse_import_mcps`, `unbrowse_local_mcps`                    | `mcps import`, `mcps`                                                           |
| `unbrowse_map`                                                   | `map <url>`                                                                     |
| `unbrowse_sites`                                                 | `sites [site]`                                                                  |
| `unbrowse_forget`                                                | `forget <capability>`                                                           |
| `unbrowse_connect`                                               | `connect [app]`                                                                 |
| `unbrowse_credits`                                               | `credits`                                                                       |
| `unbrowse_browse_open`, `_snapshot`, `_act`, `_finish`, `_close` | `browse open`, `browse snapshot`, `browse act`, `browse finish`, `browse close` |
| `unbrowse_credentials_list`, `_request`, `_status`               | `credentials list`, `credentials request`, `credentials status`                 |
| `unbrowse_replay_list`, `_search`, `_get`, `_timeline`, `_ask`   | `replay list`, `replay search`, `replay get`, `replay timeline`, `replay ask`   |
| your learned and indexed site tools (`my__…`, `skill_…`)         | `tool <name>` (below)                                                           |

### Site and account

| Command               | Does                                                                                                                                                                                                             |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `map <url>`           | A site's URLs: its sitemaps, then the page's links. `--search TEXT`, `--limit N`                                                                                                                                 |
| `sites [site]`        | What this workspace knows about each site it touched: access, saved login, session                                                                                                                               |
| `forget <capability>` | Delete one of your learned capabilities                                                                                                                                                                          |
| `connect [app]`       | Connect an app's own MCP server (`linear.app`, `notion.so`…) so its tools run as you; an app with sign-in returns a link. `--url URL --name N` adds a server not in the catalog, `--search TEXT`, `--disconnect` |
| `credits`             | Credits left; `--pack p10\|p50\|p200` returns a checkout link                                                                                                                                                    |

### Cloud browser

| Command                                          | Does                                                                                                                                                                                                                                                         |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `browse open <url>`                              | Opens a recorded cloud browser; prints the page text and its elements with `@refs`. `--task TEXT`, `--country XX`                                                                                                                                            |
| `browse snapshot <sessionId>`                    | The page now. `--text-offset N` for the next part of a long page                                                                                                                                                                                             |
| `browse act <sessionId> <action> [ref] [value…]` | `click`, `fill`, `select`, `check`, `press`, `wait` or `autofill`. `--name` names a filled input (`query`, `date`) so the learned tool gets that parameter; `--vault username\|email\|password\|totp` fills from your saved login; `--key Enter` for `press` |
| `browse finish <sessionId>`                      | Ends the session and learns the site's routes from it. `--title T`, `--goal G`                                                                                                                                                                               |
| `browse close <sessionId>`                       | Ends it; it still indexes unless `--discard`                                                                                                                                                                                                                 |

A session left open holds a browser until it idles out (2 minutes). The browser runs in Unbrowse's cloud, not on this machine.

```sh
unbrowse browse open https://example.com --task "find the pricing"
unbrowse browse act bs_1bdwuq click e1
unbrowse browse act bs_1bdwuq fill e3 serde --name query
unbrowse browse finish bs_1bdwuq --title "Search the docs"
```

### Logins

| Command                          | Does                                                                                                                               |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `credentials list [origin]`      | Saved logins usable on a site, masked. `--query TEXT`, `--limit N`                                                                 |
| `credentials request <origin>`   | A one-time link for the person to save a login you need, without you seeing it. `--fields username,password,totp`, `--reason TEXT` |
| `credentials status <requestId>` | Whether they did. `--wait-seconds N` waits for it                                                                                  |

### Replay

| Command                  | Does                                                                         |
| ------------------------ | ---------------------------------------------------------------------------- |
| `replay list`            | Recorded sessions, newest first                                              |
| `replay search <query…>` | Search what happened in them, in plain words                                 |
| `replay get <sid>`       | One session: details, signals and summary                                    |
| `replay timeline <sid>`  | One session as text, a line per event. `--from MS`, `--to MS`, `--kinds a,b` |
| `replay ask <question…>` | Ask a question across sessions                                               |

Filters for `list`, `search` and `ask`: `--site`, `--source`, `--outcome`, `--has-error`, `--audience`, `--tag`, `--from`, `--to`, `--limit`.

### Any tool by name

| Command                 | Route                                                                                               |
| ----------------------- | --------------------------------------------------------------------------------------------------- |
| `tools [query \| name]` | MCP `tools/list`: every tool the hosted MCP lists you, your learned and indexed site tools included |
| `tool <name> [JSON]`    | MCP `tools/call` with the tool's inputs                                                             |

For tools that have no command of their own, chiefly your workspace's learned and indexed site tools. `tools` prints one line per tool (`name(input, optional?)` and its first sentence); a query keeps tools whose name has a word starting with each word of it; one exact name prints its description and schema; `--json` prints every schema. `tool` takes the name with or without `unbrowse_`, with `_`, `-` or `.`; inputs are a JSON object argument or `--input JSON`, then `--key value` and `--set key=value`. `--from-here` sends `run`'s site requests from this machine.

## Your agents' MCP servers

| Command               | Route                                                                                                    |
| --------------------- | -------------------------------------------------------------------------------------------------------- |
| `mcps`                | None: reads this machine's agent configs. No account needed                                              |
| `mcps import [name…]` | `POST /connectors/import`; with `--with-keys`, `POST /connectors/connect` for each server that has a key |

`mcps` lists the MCP servers configured in Claude Code (`~/.claude.json`, the project's `.mcp.json`), Claude Desktop, Cursor (`~/.cursor/mcp.json`, `.cursor/mcp.json`), Codex (`~/.codex/config.toml`), VS Code (`mcp.json`), Windsurf, Gemini and Zed. Headers and env vars are shown by name; values are never printed, and arguments that look like keys show as `‹hidden›`. `--json` for agents.

`mcps import` brings them into Unbrowse, so their tools run there with your own account and every agent signed in to Unbrowse gets them. Remote servers connect by address: open ones at once, OAuth ones return the sign-in link. A local bridge (`mcp-remote`, `supergateway`) imports the address it forwards to. A local (stdio) server cannot run in Unbrowse's cloud: when the catalog has the same app hosted (`@modelcontextprotocol/server-github` → GitHub), that is connected instead; otherwise it is reported `local_only` and stays in your agent. Unbrowse itself, duplicates across agents and servers already connected are skipped. `--dry-run` shows the plan and connects nothing.

Only names, addresses and package names are sent. A key in a config stays on this machine unless you add `--with-keys`: then the key of each remote server that has one (an `Authorization` or api-key header with a value, not a `${VAR}` reference) is sent to Unbrowse and sealed in your vault. It asks first; scripts pass `--yes`. Exit 1 when any server failed.

## Local MCP proxy

`unbrowse mcp` serves MCP over stdio and forwards every call to the hosted MCP (`<origin>/api/mcp`, or `--url` / `UNBROWSE_MCP_URL`) with the CLI's credentials: `UNBROWSE_API_KEY`, `login --key`, or the `login` OAuth token, refreshed per call. The hosted tools are already named `unbrowse_x`; any other name with a character outside `[A-Za-z0-9_-]` is rewritten for hosts such as Grok Build and mapped back on each call. `--end-user ID` (or `UNBROWSE_END_USER`) sends `X-Unbrowse-End-User` for org keys. Nothing but protocol goes to stdout. `--from-here` (or `UNBROWSE_EGRESS=client`) runs `unbrowse_run` the way `unbrowse run` does: its site requests go from this machine, with its IP, each noted on stderr. Use it where the agent has its own computer, such as a Grok Bot (`npx -y unbrowse mcp --from-here` as a custom stdio server). It changes the IP only: it does not carry a sign-in, and a run from here goes signed out (one that needs a kept sign-in is sent through Unbrowse). For a page behind a site's sign-in, use `cookies sync`. Every other tool, and `unbrowse_resume` on that run, still goes through the hosted server; `select` is not applied locally. It also answers one tool itself, `unbrowse_local_mcps`: the servers in this machine's agent configs (no values), and with `import: true` the same import as `mcps import` (never with keys).

```json
{"mcpServers":{"unbrowse":{"command":"npx","args":["-y","unbrowse","mcp"]}}}
```

### One sign-in for the CLI and MCP

`unbrowse mcp add [claude] [codex]` registers that proxy with Claude Code (user scope) and Codex through their own `mcp add` commands, for whichever is on PATH when none is named. They then run on the CLI's sign-in: nothing to sign in to twice, and no token in their config (the proxy reads the CLI's sign-in on each call). A server already named `unbrowse` (checked with `<host> mcp get`) is left alone: `--replace` removes it first, `--name` picks another name, `--dry-run` prints the commands. Hosts start servers with a reduced environment, so `UNBROWSE_CONFIG_DIR`, `XDG_CONFIG_HOME`, `CLAUDE_CONFIG_DIR` and `UNBROWSE_SHARED_SIGNIN` are passed on when set. `UNBROWSE_API_KEY` never is (it would be a secret in the host's config): a CLI signed in only through it, or only through Claude Code's sign-in, is told to run `unbrowse login`. On Windows the host commands run through `cmd /c`. When Claude Code also has the plugin's own server (`plugin:unbrowse:unbrowse`), `mcp add` says so: with both, every tool is listed twice.

The other way: when the CLI has no sign-in of its own (no `UNBROWSE_API_KEY`, `login` or v1 key), it uses the Unbrowse sign-in Claude Code made for its MCP server, read from `~/.claude/.credentials.json` (or `$CLAUDE_CONFIG_DIR`). Only a token for this origin's REST audience counts: the Claude Code plugin's `https://unbrowse.ai/api/mcp` server has one; a server added as `https://unbrowse.ai/mcp` holds a `/mcp` token, which the REST API refuses. The token is borrowed, never refreshed (refreshing would sign Claude Code out); once it expires, Claude Code renews it on its next call or you run `unbrowse login`. `whoami` names the source. When several match (two accounts, a stale copy), the one valid longest is used. A borrowed token only goes to its own origin: `unbrowse mcp --url <another origin>` sends none. On macOS Claude Code keeps these tokens in the Keychain, so nothing is found there. `UNBROWSE_SHARED_SIGNIN=0` turns it off.

The remote MCP (`/mcp`) is a separate client. This CLI does not speak it and does not install it. Connect an agent with the commands in [install.md](/install.md) and [mcp.md](/mcp.md).

## Exit codes

| Code | Meaning                                                |
| ---- | ------------------------------------------------------ |
| 0    | Done; for runs, a verified success                     |
| 1    | Error                                                  |
| 2    | `input_required`: answer with `unbrowse resume`        |
| 3    | Not signed in, or the site needs a login nobody saved  |
| 4    | `succeeded` without verification, or `outcome_unknown` |

## Environment

| Variable              | Default                                                       |
| --------------------- | ------------------------------------------------------------- |
| `UNBROWSE_API_KEY`    | —                                                             |
| `UNBROWSE_ACCOUNT`    | unset: the account `accounts use` chose, else the default one |
| `UNBROWSE_BASE_URL`   | `https://unbrowse.ai`                                         |
| `UNBROWSE_CONFIG_DIR` | `~/.config/unbrowse`                                          |
| `UNBROWSE_NO_OPEN`    | unset; set it to never open a browser                         |
