Metadata-Version: 2.5
Name: patchright-cli
Version: 0.12.1
Summary: Anti-detect browser automation CLI using Patchright (undetected Playwright fork)
Project-URL: Homepage, https://github.com/AhaiMk01/patchright-cli
Project-URL: Repository, https://github.com/AhaiMk01/patchright-cli
Project-URL: Issues, https://github.com/AhaiMk01/patchright-cli/issues
Author: zpahai
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: anti-detect,automation,browser,cli,patchright,playwright,scraping
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.12
Requires-Dist: aiohttp>=3.9
Requires-Dist: click
Requires-Dist: patchright<1.65,>=1.64.4
Requires-Dist: pywin32>=306; sys_platform == 'win32'
Requires-Dist: pyyaml
Provides-Extra: dev
Requires-Dist: fastmcp<5,>=3.4.3; extra == 'dev'
Requires-Dist: pre-commit; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: fastmcp<5,>=3.4.3; extra == 'mcp'
Description-Content-Type: text/markdown

# patchright-cli

[![PyPI version](https://img.shields.io/pypi/v/patchright-cli?color=blue&label=PyPI)](https://pypi.org/project/patchright-cli/)
[![Python](https://img.shields.io/pypi/pyversions/patchright-cli?label=Python)](https://pypi.org/project/patchright-cli/)
[![Downloads](https://img.shields.io/pypi/dm/patchright-cli?color=orange&label=Downloads)](https://pypi.org/project/patchright-cli/)
[![License](https://img.shields.io/github/license/AhaiMk01/patchright-cli?color=green)](LICENSE)
[![CI](https://img.shields.io/github/actions/workflow/status/AhaiMk01/patchright-cli/publish.yml?label=CI)](https://github.com/AhaiMk01/patchright-cli/actions)
[![GitHub stars](https://img.shields.io/github/stars/AhaiMk01/patchright-cli?style=flat&label=Stars)](https://github.com/AhaiMk01/patchright-cli)
[![GitHub issues](https://img.shields.io/github/issues/AhaiMk01/patchright-cli?label=Issues)](https://github.com/AhaiMk01/patchright-cli/issues)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)

Anti-detect browser automation CLI. Commands follow [Microsoft's playwright-cli](https://github.com/microsoft/playwright-cli), but it uses [Patchright](https://github.com/kaliiiiiiiiii/patchright-python) (undetected Playwright fork) to bypass bot detection.

[Install](#install) · [Quick Start](#quick-start) · [Commands](#-commands) · [Snapshots](#snapshots) · [Agent Integration](#agent-integration)

### Highlights

| | Feature |
|---|---|
| :shield: | Bypasses Akamai, Cloudflare, and other anti-bot systems |
| :rocket: | playwright-cli's command names and arguments — little to relearn |
| :robot: | Built for AI agents (Claude Code, Codex, etc.) with YAML snapshots |
| :zap: | Daemon architecture — browser stays open between commands |
| :package: | `pip install` / `uvx` — no Docker, no config files |
| :lock: | Persistent profiles for maintaining login sessions |

---

## Install

> [!TIP]
> **Just paste this into your AI coding agent — it will do the rest:**
>
> ```
> Install and set up patchright-cli by following the instructions here:
> https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/docs/installation.md
> ```
>
> Your agent handles the install, browser setup, and skill configuration automatically.

<details>
<summary><b>For LLM Agents</b></summary>

```bash
curl -s https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/docs/installation.md
```

</details>

> [!IMPORTANT]
> **Requirements:** Python 3.12+ and Google Chrome

```bash
# Recommended — always runs latest version, no install needed
uvx patchright-cli open https://example.com
```

<details>
<summary><b>Other install methods</b></summary>

```bash
# Via pip
pip install patchright-cli
patchright-cli open https://example.com
patchright-cli close

# Update
pip install --upgrade patchright-cli
```

**From source:**

```bash
git clone https://github.com/AhaiMk01/patchright-cli.git
cd patchright-cli
uv venv && uv pip install -e .
python -m patchright install chromium
```

</details>

### Docker

A prebuilt image is available from GHCR:

```bash
docker pull ghcr.io/ahaimk01/patchright-cli:latest
```

By default the container runs `patchright-cli` directly, so it can be used for normal CLI commands:

```bash
docker run --rm ghcr.io/ahaimk01/patchright-cli:latest --help
docker run --rm ghcr.io/ahaimk01/patchright-cli:latest profile-list
```

Browser sessions live inside the container, so for a sequence of standalone CLI commands keep the same container running:

```bash
docker run --rm -it \
  --name patchright-cli \
  -v patchright-profiles:/root/.patchright-cli \
  --entrypoint /bin/sh \
  ghcr.io/ahaimk01/patchright-cli:latest

uv run patchright-cli open https://example.com --headless
uv run patchright-cli snapshot
uv run patchright-cli close
```

For MCP over stdio, set `PATCHRIGHT_CLI_MODE=mcp-stdio` and keep stdin attached. The volumes keep logins and output after the container exits:

```bash
docker run --rm -i \
  -e PATCHRIGHT_CLI_MODE=mcp-stdio \
  -v ./patchright:/patchright \
  -v patchright-profiles:/root/.patchright-cli \
  ghcr.io/ahaimk01/patchright-cli:latest
```

For a long-running MCP server over Streamable HTTP, set `PATCHRIGHT_CLI_MODE=mcp-http`:

```bash
docker run -d \
  --name patchright-mcp \
  -p 127.0.0.1:9090:9090 \
  -e PATCHRIGHT_CLI_MODE=mcp-http \
  -e PATCHRIGHT_CLI_MCP_TOKEN=replace-with-a-long-random-token \
  -v ./patchright:/patchright \
  -v patchright-profiles:/root/.patchright-cli \
  ghcr.io/ahaimk01/patchright-cli:latest
```

The HTTP MCP endpoint is then available at `http://localhost:9090/mcp`. The MCP server defaults to headless mode in the image and uses `/patchright` as its filesystem root. CLI commands inside the container still need `--headless`, since there is no display.

An example `compose.yml` is included in the repository:

```bash
PATCHRIGHT_CLI_MCP_TOKEN=replace-with-a-long-random-token docker compose up -d
```

Both examples publish the port on `127.0.0.1` only, but that alone doesn't stop a web page in your browser from reaching the server through DNS rebinding, so `mcp-http` refuses to start unless you pick one of three defences (see [MCP Server](#mcp-server) for details): set `PATCHRIGHT_CLI_MCP_TOKEN`; set `-e FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=true` so requests must be addressed to `localhost`, `127.0.0.1` or `::1`, listing any other hostname or IP (a LAN address, a Compose service name) as a JSON array, for example `-e FASTMCP_HTTP_ALLOWED_HOSTS='["patchright-mcp", "192.168.1.20"]'`; or set `-e FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=false` on purpose. A plain `docker run -e PATCHRIGHT_CLI_MODE=mcp-http` with neither variable exits with an error. The example `compose.yml` sets `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION: "false"` so reverse-proxy and MCP hub setups keep working, which makes the token optional there: leave it out if your MCP client can't send an `Authorization` header. A reverse proxy with auth or an IP allowlist can stand in for the token. If you publish the port beyond loopback, protect it one of these ways: the endpoint gives full control of the browser and its saved logins.

### Chrome sandbox

Patchright starts Chrome with `--no-sandbox`, so by default no Chrome process is sandboxed (`chrome://sandbox` lists every one as "Not Sandboxed", on Windows too). Set `PATCHRIGHT_CLI_CHROME_SANDBOX=1` to launch Chrome with its sandbox instead. It is off by default.

The variable is read by the daemon, not by each CLI command. The CLI passes its environment on when it starts the daemon, so set it before the first `open`; a daemon that is already running keeps the setting it started with until you restart it (`patchright-cli kill-all`).

In Docker the image runs as root, and Chrome will not start as root with its sandbox on, so the switch also needs a non-root user (`docker run --user ...` with a writable `HOME`). Even then, Docker's default seccomp profile blocks the namespaces Chrome's Linux sandbox needs: measured on a GitHub Actions runner, as uid 1000 with the switch on, Chrome exits at launch ("Failed to move to new namespace ... Operation not permitted"). Leave the switch off in Docker; the image runs Chrome without its sandbox.

How the sandbox affects bot detection has not been measured.

---

## Quick Start

```bash
# Launch undetected Chrome and navigate
patchright-cli open https://example.com

# Take a snapshot to see interactive elements with refs
patchright-cli snapshot

# Interact using refs from the snapshot
patchright-cli click e2
patchright-cli fill e5 "search query"
patchright-cli press Enter

# Take a screenshot
patchright-cli screenshot

# Close the browser
patchright-cli close
```

Quote URLs that contain `&`; unquoted, bash, PowerShell and cmd.exe all split the command there. On Windows two cases need more than quotes: in cmd.exe without quotes, escape it as `^&` (inside quotes the `^` stays in the URL, so use one or the other), and when PowerShell runs a `.cmd`/`.bat` wrapper such as the repo's `patchright-cli.cmd`, PowerShell drops the quotes and cmd.exe splits at `&`, so pass the rest verbatim with `--%`:

```
patchright-cli goto "https://example.com/?a=1&b=2"           # any shell, patchright-cli.exe
patchright-cli goto https://example.com/?a=1^&b=2            # cmd.exe, unquoted
.\patchright-cli.cmd --% goto "https://example.com/?a=1&b=2" # PowerShell, .cmd wrapper
```

Don't use `--%` with the installed `patchright-cli.exe`: in PowerShell 7 the quotes after it reach the CLI and end up in the URL.

---

## Architecture

```mermaid
graph LR
    A[CLI] -->|TCP:9321| B[Daemon]
    B -->|Patchright| C[Chrome]
```

| Component | Role |
|-----------|------|
| **Daemon** (`daemon.py`) | Long-running process managing browser sessions via Patchright. Auto-starts on first `open`. |
| **CLI** (`cli.py`) | Thin client — connects, sends command, prints result, disconnects. Browser stays open. |
| **Snapshot** (`snapshot.py`) | Playwright's AI-mode `aria_snapshot()` accessibility tree, iframes included, with Playwright's own `[ref=e5]` handles, resolved with `get_by_ref`. |

### Daemon token

The daemon only listens on `127.0.0.1`, but any local process can reach that port, so every message must carry a secret token. On startup the daemon writes a fresh random token to `~/.patchright-cli/daemon-<port>.token` (one file per port, so daemons on different ports coexist), before it starts listening, and deletes the file on a clean shutdown. The CLI (and the MCP server, which runs the CLI) reads the file for its `--port` and sends the token with each command; a message with a missing or wrong token is rejected and nothing runs.

- On Linux/macOS the file is created with mode `0600`. On Windows, POSIX modes do not apply, so the file is created with its own protected ACL: one full-access entry for your user account and nothing inherited from `~/.patchright-cli`, whatever that directory grants. If the daemon cannot create the file that way it does not start.
- A daemon holds `~/.patchright-cli/daemon-<port>.lock` while it runs, so only one daemon per port ever writes that port's token; a second daemon started on the same port exits without touching it. The lock file stays after shutdown and is harmless (the OS releases the lock when the process exits).
- Run the CLI as the same user, with the same home directory, as the daemon. In Docker the MCP server and daemon share a user and `HOME`, so this holds.
- An older CLI talking to a newer daemon gets an error saying the daemon requires an auth token: upgrade, or stop the daemon (`patchright-cli kill-all` from the newer install, or end the process). A newer CLI talking to an older daemon finds no token file and sends none, which the older daemon accepts.

### Stopping a stuck daemon

The daemon runs without a window. `kill-all` asks it to close every session and exit. If it does not answer within 20 seconds, or with `--force`, the CLI ends the daemon process and the browsers it launched (`taskkill /T` on Windows, the daemon's process group elsewhere). It finds the process through `~/.patchright-cli/daemon-<port>.pid`, which the daemon writes at startup with its PID and start time, and kills only a process whose start time still matches, so a reused PID is never touched. A daemon from a version that writes no PID file has to be ended by hand: its command line contains `patchright_cli.daemon`.

### Daemon client protocol

Third-party clients can use the daemon TCP interface directly. The
[client protocol reference](docs/daemon-protocol.md) defines discovery,
authentication, message framing, replies, and compatibility rules.

---

<details>
<summary><h2>📖 Commands</h2></summary>

### Core
```bash
patchright-cli open [url]              # Launch browser
patchright-cli open --profile=work     # Named profile any session can use (logins persist)
patchright-cli open --headless         # Run headless (later opens on this daemon stay headless)
patchright-cli open --headed           # Headed window even on a headless daemon
patchright-cli open --background       # Headed Chrome whose windows and tabs don't take desktop focus
patchright-cli open --profile=./dir    # A value with / or \ is a profile directory
patchright-cli --tab <name> open <url> # Named tab: shared browser, own page/refs
patchright-cli open --mobile           # Emulate a generic mobile device (Pixel 7)
patchright-cli open --device="Pixel 5" # Emulate a specific Playwright device
patchright-cli goto <url>              # Navigate
patchright-cli click <ref>             # Click element
patchright-cli click <ref> right       # Right-click
patchright-cli click <ref> --modifiers=Alt,Shift
patchright-cli dblclick <ref>          # Double-click
patchright-cli dblclick <ref> --modifiers=Shift
patchright-cli fill <ref> <value>      # Fill text input
patchright-cli fill <ref> <value> --submit  # Fill and press Enter
patchright-cli type <text>             # Type via keyboard
patchright-cli type <text> --submit    # Type and press Enter
patchright-cli hover <ref>             # Hover over element
patchright-cli select <ref> <value>    # Select dropdown option
patchright-cli check <ref>             # Check checkbox
patchright-cli uncheck <ref>           # Uncheck checkbox
patchright-cli drag <from> <to>        # Drag and drop
patchright-cli snapshot                # Accessibility snapshot
patchright-cli snapshot -i             # Interactive elements only (recommended)
patchright-cli snapshot --depth=3      # Limit depth
patchright-cli snapshot --selector=CSS  # Snapshot a CSS-scoped subtree
patchright-cli snapshot <ref>          # Snapshot element subtree
patchright-cli snapshot --filename=f   # Save to custom path
patchright-cli find "text"             # Search snapshot for text
patchright-cli find --regex "a|b"      # Search by regex
patchright-cli find "text" --all       # Include text/paragraph nodes
patchright-cli find "text" --limit=50  # Cap hits (default 20)
patchright-cli find --role=button --description="Deletes"  # By role + accessible name/description
patchright-cli html                    # Page HTML (first 8000 chars; --max-chars=0 for all)
patchright-cli html --include-shadow --filename=page.html  # With open shadow roots, to a file
patchright-cli html e5                 # One element's outerHTML (or --selector=CSS)
patchright-cli eval <expr>             # Run JavaScript
patchright-cli eval --file=script.js   # Run JS from file
patchright-cli run-code <code>         # Run JS with return value
patchright-cli run-code --file=f.js    # Run JS from file
patchright-cli screenshot              # Page screenshot
patchright-cli screenshot --full-page  # Full scrollable page
patchright-cli screenshot <ref>        # Element screenshot
patchright-cli screenshot --filename=f # Save to custom path
patchright-cli screenshot --hires      # Capture at device pixel ratio
patchright-cli close                   # Close session
```

### Navigation
```bash
patchright-cli go-back
patchright-cli go-forward
patchright-cli reload
patchright-cli wait 500                 # Wait a fixed number of milliseconds
patchright-cli wait --url="**/dashboard" # Wait for the URL to match (`*` never crosses a `/`)
patchright-cli wait-for <ref>           # Wait until an element is visible
patchright-cli wait-for <ref> --state=hidden
```

### Keyboard / Mouse
```bash
patchright-cli press Enter
patchright-cli keydown Shift
patchright-cli keyup Shift
patchright-cli mousemove 150 300
patchright-cli mousedown [button]
patchright-cli mouseup [button]
patchright-cli mousewheel 0 100
```

### Dialog
```bash
patchright-cli dialog-accept [text]    # Accept next alert/confirm/prompt
patchright-cli dialog-dismiss          # Dismiss next dialog
```

### Upload / Resize
```bash
patchright-cli upload ./file.pdf       # Upload to first file input
patchright-cli upload ./file.pdf e5    # Upload to specific input
patchright-cli resize 1920 1080        # Resize viewport
```

### Emulation
```bash
patchright-cli set-color-scheme dark   # dark | light | no-preference
patchright-cli clear-color-scheme
patchright-cli set-reduced-motion reduce  # reduce | no-preference
patchright-cli clear-reduced-motion
patchright-cli set-forced-colors active   # active | none
patchright-cli clear-forced-colors
patchright-cli set-contrast more       # more | no-preference
patchright-cli clear-contrast
patchright-cli set-media print         # screen | print
patchright-cli clear-media
```

Each setting holds for the whole session, including tabs and popups opened later; `clear-*` drops just that one, back to the browser default. A popup gets the setting just after it opens, so a script that reads media queries the instant the popup is created may still see the default.

### Tabs
```bash
patchright-cli tab-list
patchright-cli tab-new [url]
patchright-cli tab-select <index>
patchright-cli tab-close [index]
```

### State Persistence
```bash
patchright-cli state-save [file]       # Save cookies + localStorage
patchright-cli state-load <file>       # Restore saved state
```

### Storage
```bash
# Cookies
patchright-cli cookie-list
patchright-cli cookie-list --domain=example.com
patchright-cli cookie-list --path=/api
patchright-cli cookie-get <name>
patchright-cli cookie-set <name> <value>
patchright-cli cookie-set <name> <value> --domain=example.com --httpOnly --secure
patchright-cli cookie-set <name> <value> --path=/ --sameSite=Lax --expires=1735689600
patchright-cli cookie-delete <name>
patchright-cli cookie-clear

# localStorage
patchright-cli localstorage-list
patchright-cli localstorage-get <key>
patchright-cli localstorage-set <key> <value>
patchright-cli localstorage-delete <key>
patchright-cli localstorage-clear

# sessionStorage
patchright-cli sessionstorage-list
patchright-cli sessionstorage-get <key>
patchright-cli sessionstorage-set <key> <value>
patchright-cli sessionstorage-delete <key>
patchright-cli sessionstorage-clear
```

### Passkeys
A virtual WebAuthn authenticator for testing passkey sign-ups and logins. The first `passkey-*` command installs it in the session, overriding `navigator.credentials.create()` / `.get()` in every page (a page can notice the override, so it stays off until asked for). **Run a `passkey-*` command (e.g. `passkey-list`) before the page calls WebAuthn**: until then the page gets Chrome's native OS passkey dialog and hangs. Passkeys live in the session only; use `passkey-export` / `passkey-import` to keep them (the export holds private keys and is written owner-only; keep it out of version control).
```bash
patchright-cli passkey-list                      # Installs the authenticator, lists passkeys (no keys shown)
patchright-cli passkey-list --rp-id=example.com
patchright-cli passkey-add example.com           # Seed a new P-256 passkey
patchright-cli passkey-add example.com --id=<b64url> --user-handle=<b64url> --private-key=<b64url> --public-key=<b64url> --sign-count=5
patchright-cli passkey-delete <credential-id>
patchright-cli passkey-clear [--rp-id=example.com]
patchright-cli passkey-export passkeys.json      # Includes private keys
patchright-cli passkey-import passkeys.json
```

### Request Mocking
```bash
patchright-cli route "**/*.jpg" --status=404
patchright-cli route "https://api.example.com/**" --body='{"mock":true}'
patchright-cli route "**/*" --content-type=application/json --body='{"ok":true}'
patchright-cli route "**/*" --header=X-Custom:value
patchright-cli route "**/*" --remove-header=Content-Type
patchright-cli route-list
patchright-cli unroute "**/*.jpg"
patchright-cli unroute                 # Remove all routes
```

### Tracing / Video / PDF
```bash
patchright-cli tracing-start
patchright-cli tracing-stop            # Saves .zip trace file
patchright-cli video-start             # Start real-time video recording at 25 fps (CDP screencast)
patchright-cli video-start --fps=60    # Another frame rate, 1-60
patchright-cli video-stop              # Stop and save video (requires ffmpeg for .webm)
patchright-cli video-stop --filename=recording.webm
patchright-cli show --annotate          # Ask a human to mark up the page, wait for it
patchright-cli video-show-actions       # Annotate each action in the recording
patchright-cli video-hide-actions       # Stop annotating actions
patchright-cli pdf --filename=page.pdf
```

### Network
```bash
patchright-cli network                 # Network request log
patchright-cli network --static        # Include static resources
patchright-cli network --clear         # Clear log after printing
patchright-cli network-state-set offline  # Simulate offline mode
patchright-cli network-state-set online   # Restore connectivity
```

### DevTools
```bash
patchright-cli console                 # Start capture / show captured console messages
patchright-cli console warning         # Filter by level
patchright-cli console --clear         # Clear after printing
```

The first `console` call enables CDP Runtime capture for every page in the session,
including tabs and popups opened later. Messages from before capture starts are
unavailable: run `console` before reproducing an issue, then call it again to read
the messages. Capture can make automation detectable and stays enabled until the
session closes; `--clear` only clears the message buffer. A new session starts
with capture disabled.

Two DevTools commands leave traces a page can see. `highlight` draws inside an
`<x-pw-glass>` element under `<html>`; `highlight --hide` removes it once no
highlight is left. `pick-locator` runs only with `--detectable`: it installs
Playwright's recorder, which puts `window.playwright`, `__pw_*` globals and an
`<x-pw-glass>` element into every page of the session until the session closes.

### Output
```bash
patchright-cli --raw eval --file=s.js  # Raw output (no page/snapshot decoration)
patchright-cli --raw snapshot > p.yml  # Pipe raw snapshot to file
patchright-cli --json tab-list         # Whole reply as JSON ({success, output, data, ...})
patchright-cli --toon tab-list         # Structured result as TOON
```

`--toon` prints a reply's structured `data` as [TOON](https://github.com/toon-format/spec), a compact
format for agents: `list`, `tab-list`, `cookie-list`, `localstorage-list`, `sessionstorage-list`,
`route-list`, `console`, `network`, `profile-list` and `find` give a table, `url` and `title` a single
field. A cut-short list adds `total` and a `help:` line. Commands without structured data, and errors,
print exactly as they do without the flag. `--json` and `--toon` together are a usage error.

```
tabs[2]{index,title,url,active}:
  0,Example Domain,"https://example.com/",true
  1,"","about:blank",false
```

Run with no arguments, `patchright-cli` prints a short home view instead of the manual (that is
`--help`): where the binary is, whether the daemon is running on the port (`--port=N` picks another),
the open sessions with their URL and tab count, and what to run next. It only asks a daemon that is
already running and never starts one. Add `--toon` or `--json` for the same state as data.

```
~/.local/bin/patchright-cli v0.10.0 -- anti-detect Chrome automation CLI (playwright-cli style commands)
daemon: running on port 9321 (1 session)
sessions[1]{name,status,url,tabs}:
  default,open,"https://example.com/",2
help: patchright-cli snapshot  (element refs for the current page; -s=<name> for another session)
help: patchright-cli list  (sessions and their tabs)
help: patchright-cli --help  (all commands); patchright-cli <command> --help  (one command)
```

### Setup
```bash
patchright-cli install --skills        # Auto-install skills for detected AI agents
```

### Sessions
```bash
patchright-cli -s=mysession open https://example.com
patchright-cli -s=mysession click e6
patchright-cli -s=mysession close
patchright-cli list                    # List all sessions
patchright-cli close-all
patchright-cli kill-all                # Stop the daemon; force-stops it if it does not answer
patchright-cli kill-all --force        # Kill the daemon and its browsers without asking
patchright-cli delete-data             # Delete the profile named after the session
patchright-cli profile-list           # Profiles, size, and which session uses each
patchright-cli profile-delete <name>  # Delete an idle named profile
patchright-cli --port=9322 open        # Custom daemon port
```

</details>

---

## Agent Integration

The CLI follows the [AXI](https://github.com/kunchenguid/axi) standard for agent-facing tools: errors and hints on stdout, exit 2 for usage errors, `(no-op)` for repeated actions, and a no-argument home view with live state.

There are two ways to teach your agent about patchright-cli. Both get it to the same place, so you only need one.

| | Session hook | Skill |
|---|---|---|
| Install | `patchright-cli install --hooks` | `npx skills add AhaiMk01/patchright-cli` or `patchright-cli install --skills` |
| What the agent gets | Live state (open sessions, the commands to drive them) at the start of every session | Full command reference, loaded when a browser task comes up |
| Cost | A few lines of context in every session | Nothing until it is needed |
| Agents | Claude Code, Codex, OpenCode | Claude Code, Cursor, Gemini CLI, Codex and [40+ others](https://github.com/vercel-labs/skills) |

### Session hook (recommended)

```bash
patchright-cli install --hooks              # install or repair
patchright-cli install --hooks --uninstall  # remove
```

At the start of each session the agent runs `patchright-cli` with no arguments, which prints the home view (see [Output](#output)): whether the daemon is up and which sessions are open, so the agent can pick up an open browser session without asking. The hook goes only to apps whose config directory exists, and prints one line per app (`installed`, `updated`, `(no-op)` or `skipped`):

- **Claude Code**: a `SessionStart` hook in `~/.claude/settings.json`
- **Codex**: a `SessionStart` hook in `~/.codex/hooks.json`, plus `hooks = true` under `[features]` in `~/.codex/config.toml`
- **OpenCode**: a managed plugin at `~/.config/opencode/plugins/patchright-cli.js` that adds the dashboard to the system prompt

The hook calls plain `patchright-cli` when that name on your `PATH` is the same binary you ran the install with, and its absolute path otherwise. Re-run `install --hooks` after reinstalling or moving patchright-cli and it rewrites the path. Your other hooks and settings are left as they are, and `--uninstall` removes only patchright-cli's entries (the Codex `[features]` flag stays, since other hooks may need it).

### Skill

```bash
# Install skill for all detected agents
npx skills add AhaiMk01/patchright-cli
```

<details>
<summary><b>Other install methods</b></summary>

```bash
# Via patchright-cli itself
pip install patchright-cli
patchright-cli install --skills

# Or just tell your agent:
# "Install patchright-cli skill from https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md"
```

</details>

### MCP server

MCP support is optional so the base CLI install stays small:

```bash
pip install 'patchright-cli[mcp]'

# stdio (default)
patchright-cli mcp

# Streamable HTTP (needs a token or an explicit Host/Origin choice, see below)
PATCHRIGHT_CLI_MCP_TOKEN=replace-with-a-long-random-token patchright-cli mcp --http --host=127.0.0.1 --port=8000
```

![How a remote MCP call reaches Chrome](docs/mcp-architecture.svg)

The HTTP endpoint is `/mcp`. Set `PATCHRIGHT_CLI_MCP_TOKEN` to require `Authorization: Bearer <token>` on every request, and always set it before binding to `0.0.0.0`.

FastMCP's Host and Origin checks are off by default, so binding to `127.0.0.1` alone does not stop a web page from reaching a server without a token through DNS rebinding. `patchright-cli mcp --http` therefore refuses to start, whatever the bind host, unless `PATCHRIGHT_CLI_MCP_TOKEN` is set or `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION` is set to a value FastMCP accepts (an empty value counts as unset). Pick one:

- Set `PATCHRIGHT_CLI_MCP_TOKEN`.
- Set `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=true` to accept only requests addressed to `localhost`, `127.0.0.1` or `::1` (421 otherwise) and reject foreign `Origin` headers (403). To allow another hostname or IP, list it as a JSON array, for example `FASTMCP_HTTP_ALLOWED_HOSTS='["mcp.lan"]'`.
- Set `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=false` on purpose, for example behind a reverse proxy with auth or an IP allowlist.

A reverse proxy with auth or an IP allowlist can stand in for the token. The stdio transport needs none of this.

One token covers one trust domain. Any client holding it can open, read or close any session by name, and `patchright_kill_all` closes them all. Clients that must not reach each other's browsers need separate servers, each with its own token. Two servers on one host share the daemon unless their `PATCHRIGHT_CLI_MCP_DAEMON_PORT` values differ, and they always share saved profiles in `~/.patchright-cli/profiles`. For full separation, run each server in its own container or as a different OS user.

File paths passed to MCP tools (uploads, state and cookie files, screenshot/PDF/snapshot output, profile directories) must be absolute and beneath the MCP root, so a client cannot reach arbitrary host files. Outputs without a path land in `<root>/output/`, which `browser_delete_output` can clean up. `patchright_eval` and `patchright_run_code` still run arbitrary JavaScript in the page, so protect HTTP deployments regardless. The MCP server never loads a `.patchright-cli/config.json`, since clients can write files beneath the root; tool arguments carry every option.

URLs passed to `patchright_open`, `patchright_goto` and `patchright_tab_new` must be `http`, `https` or `about:blank`, so the browser cannot read local files through `file://` (allowed again with `PATCHRIGHT_CLI_MCP_UNRESTRICTED_FILESYSTEM=1`). The server sends `--web-only` with every call, so the daemon holds the navigations it starts itself to the same rule (a link `patchright_click` follows, `patchright_go_back`, `patchright_go_forward`), also in a session you opened with the plain CLI, whose own commands stay unrestricted. A client can still read whatever such a session already shows, a `file://` page included, so to keep your own CLI sessions out of reach give the server its own daemon with `PATCHRIGHT_CLI_MCP_DAEMON_PORT`. Sessions the server opens keep the option, and the daemon sends a frame in them that still lands on another scheme back to `about:blank`. The browser also stays off `169.254.0.0/16` and `fe80::/10`, where cloud metadata services hand out credentials: `patchright_open`, `patchright_goto`, `patchright_tab_new` and `patchright_attach` refuse those hosts (the proxy too), and a frame that still lands there through a redirect or a script is sent back to `about:blank`. Set `PATCHRIGHT_CLI_MCP_BLOCK_NETWORKS=private` to refuse every address that is not globally routable as well (loopback, RFC 1918, CGNAT, unique local), or `none` to turn the check off. It never intercepts requests, so the HTTP cache stays on, and in `attach --context=host` sessions it only checks URLs before navigating, leaving the Host's other pages alone. That also means it is not a hard boundary: images, `fetch()` and WebSockets are not checked (CORS still hides their responses from the page), and a DNS server that answers differently each time can get past it. For that, restrict the container's or host's outbound traffic.

| Variable | Default | Effect |
|---|---|---|
| `PATCHRIGHT_CLI_MCP_TOKEN` | unset (no auth) | Bearer token required by the HTTP transport |
| `PATCHRIGHT_CLI_MCP_ROOT` | working directory | Root that tool file paths must sit beneath |
| `PATCHRIGHT_CLI_MCP_UNRESTRICTED_FILESYSTEM` | unset | `1` drops the path check and enables `patchright_install_skills` |
| `PATCHRIGHT_CLI_MCP_HEADLESS` | unset (headed) | `1` makes `patchright_open` default to headless, for hosts without a display |
| `PATCHRIGHT_CLI_MCP_DAEMON_PORT` | `9321` | Daemon port the tools talk to |
| `PATCHRIGHT_CLI_MCP_BLOCK_NETWORKS` | `link-local` | Address ranges the browser may not reach: `link-local`, `private` or `none` |
| `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION` | unset (off) | `true` rejects requests whose Host is not loopback or allowed, and foreign Origins; without a token, HTTP mode needs it set to `true` or `false` |
| `FASTMCP_HTTP_ALLOWED_HOSTS` | unset | JSON array of extra hostnames the Host check accepts |

### Skill docs

| File | Topic |
|------|-------|
| [`SKILL.md`](skills/patchright-cli/SKILL.md) | Main skill — commands, options, patterns |
| [`snapshot-refs.md`](skills/patchright-cli/references/snapshot-refs.md) | Snapshots, element refs, `-i` flag |
| [`session-management.md`](skills/patchright-cli/references/session-management.md) | Named sessions, profiles, CDP, dashboard |
| [`request-mocking.md`](skills/patchright-cli/references/request-mocking.md) | Route mocking, network state |
| [`storage-state.md`](skills/patchright-cli/references/storage-state.md) | Cookies, localStorage, sessionStorage |
| [`running-code.md`](skills/patchright-cli/references/running-code.md) | eval, run-code, `--file`, stdin |
| [`tracing.md`](skills/patchright-cli/references/tracing.md) | Trace recording and debugging |
| [`video-recording.md`](skills/patchright-cli/references/video-recording.md) | Video, codegen, screenshot, PDF |

---

## Star History

<a href="https://star-history.com/#AhaiMk01/patchright-cli&Date">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=AhaiMk01/patchright-cli&type=Date&theme=dark" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=AhaiMk01/patchright-cli&type=Date" />
   <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=AhaiMk01/patchright-cli&type=Date" />
 </picture>
</a>

---

<details>
<summary><h2>Differences from playwright-cli</h2></summary>

patchright-cli follows the command names and arguments of [Microsoft's playwright-cli](https://github.com/microsoft/playwright-cli) where Patchright allows it, but it is **not a drop-in replacement**. Some playwright-cli features cannot work on Patchright and are not implemented:

| Feature | Reason |
|---------|--------|
| `--browser=firefox/webkit/msedge` | Patchright only supports Chromium/Chrome. Anti-detect patches are Chrome-specific. |
| `--extension` | Browser extension connection not supported. Patchright uses CDP directly. |
| `install` / `install-browser` | Use `python -m patchright install chromium` instead. |

Each command accepts only the flags it implements. A playwright-cli flag patchright-cli does not support, such as `open --browser=firefox`, is rejected with `error: unknown flag` and exit code 2 rather than silently ignored, so a script ported from playwright-cli fails loudly where it would otherwise behave differently. `patchright-cli <command> --help` lists a command's flags.

</details>

---

## Disclaimer

> [!WARNING]
> This tool is provided for **authorized security testing, legitimate automation, and educational purposes** only. Users are solely responsible for ensuring their use complies with applicable laws and the terms of service of any websites they interact with. The authors do not endorse or encourage any unauthorized access, scraping, or circumvention of security measures. Use at your own risk.

---

## Credits

This project builds on the work of:

- **[Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright)** by [@Kaliiiiiiiiii-Vinyzu](https://github.com/Kaliiiiiiiiii-Vinyzu) — the undetected Playwright fork that makes this possible. All anti-detect capabilities come from Patchright.
- **[playwright-cli](https://github.com/microsoft/playwright-cli)** by Microsoft — the original CLI that patchright-cli's command interface is modeled after.
- **[camoufox-cli](https://github.com/Bin-Huang/camoufox-cli)** by [@Bin-Huang](https://github.com/Bin-Huang) — inspiration for the CLI-first approach to anti-detect browser automation and the SKILL.md agent integration pattern.
- **[AXI (Agent eXperience Interface)](https://github.com/kunchenguid/axi)** by [@kunchenguid](https://github.com/kunchenguid) — the standard for agent-facing CLIs that shaped the home view, strict per-command flags, `error:`/`help:` output, `(no-op)` replies, explicit empty states and the session-start hook.
- **[TOON](https://github.com/toon-format/spec)** — the token-oriented output format behind `--toon`.

---

## License

Apache 2.0 — same as [playwright-cli](https://github.com/microsoft/playwright-cli)
