A command-line wrapper around pydoll — automate Chromium-based browsers (Chrome, Edge, Wavebox, any Chromium) over the Chrome DevTools Protocol with no WebDriver, stealth defaults, and first-class JSON output for AI agents.
Not on PyPI — install straight from GitHub.
# Recommended: isolated, upgradable tool install (Astral uv)
uv tool install git+https://github.com/dovidgef/pydoll-cli
# Alternatives
pipx install git+https://github.com/dovidgef/pydoll-cli
pip install git+https://github.com/dovidgef/pydoll-cliPin a release by appending a tag, or track master by omitting it:
uv tool install git+https://github.com/dovidgef/pydoll-cli@v0.5.1
uv tool upgrade pydoll-cli # re-resolves the git refAfter install both commands are on your PATH:
pydoll-cli --help
pydoll --help # aliasPython 3.10+ is required. Google Chrome (or Edge / Wavebox / Chromium) must be installed locally; pydoll-cli auto-detects standard install paths on Linux, macOS, and Windows, or you can pass --browser-binary /path/to/chrome.
# One-shot screenshot
pydoll-cli screenshot https://example.com -o shot.png
# Print rendered page title as JSON
pydoll-cli --output json get https://example.com
# Persistent session (fast for multi-step flows / AI agents).
# `--no-headless` is a *global* flag — it must come BEFORE the subcommand.
pydoll-cli --no-headless session start agent-run
pydoll-cli --session agent-run get https://news.ycombinator.com
pydoll-cli --output json --session agent-run query "a.storylink" --all --attr href
pydoll-cli session stop agent-run # kills the browser AND deletes its profile (--no-purge to keep)
# Reclaim disk from leftover/old session profiles (they show as "orphan" in list).
pydoll-cli session prune --orphans --dry-run # preview; add --yes to delete. Also --dead, --older-than N
pydoll-cli session rm agent-run # stop if running + delete one session outright
# Keeping a login across restarts: --no-purge marks the profile "kept", and
# prune leaves kept profiles alone (--include-kept overrides).
pydoll-cli session stop linkedin --no-purge
pydoll-cli session start linkedin # same profile, still logged in
# Attach to a running Chrome/Wavebox and operate in an isolated incognito tab
# that persists across commands (default for --browser wavebox; opt-in elsewhere)
pydoll-cli session start my-work --attach --url https://example.com
pydoll-cli --session my-work query "h1" --attr textContent
pydoll-cli session stop my-work # deletes just the incognito context
# Attach but share your real logged-in profile (cookies/logins flow through).
# If --url is already open in another tab, that tab is adopted as-is.
# session stop leaves the pinned tab open by default (--close-tab to close it).
pydoll-cli session start linkedin --attach --share-profile \
--url https://www.linkedin.com/feed/
pydoll-cli --session linkedin query "h1"
# Or: ride along on a tab the user already has open (adopt-only, never spawns).
pydoll-cli --browser wavebox session start work --share-profile \
--tab-url 'github.com/anthropics'
# Use Wavebox instead of Chrome
pydoll-cli --browser wavebox screenshot https://example.com -o shot.png
# Attach to an already-running Chrome/Wavebox (with --remote-debugging-port=9222)
# and drive a fresh incognito tab without disturbing the user's existing work
pydoll-cli --connect ws://127.0.0.1:9222/devtools/browser/XXX --fresh get https://example.com
# Bypass Cloudflare Turnstile and dump the page HTML
pydoll-cli cloudflare bypass https://example-protected.com -o page.html
# Structured extraction with a JSON schema
pydoll-cli extract https://quotes.toscrape.com \
--schema examples/quotes.json --scope ".quote" --all
# Hybrid HTTP (authenticated via the browser session)
pydoll-cli --session logged-in request GET https://my-site.com/api/user/profile
# SPA-friendly: load fast, then wait on real content
pydoll-cli --page-load-state interactive --session s get https://app.example.com
pydoll-cli --session s wait --selector ".content" --wait 30
pydoll-cli --session s query "h1"
# Aggregator with progressive results — wait for the result set to stabilize
pydoll-cli --session s wait --stable-ids ".result|data-id" --stable-ms 2000 --wait 35
# Many independent URLs in parallel (one tab each, ~10× sequential)
pydoll-cli --output json batch https://a.com https://b.com https://c.com \
--query "h1" --concurrency 3
# Network interception (wrap pattern): block heavy assets for one screenshot
pydoll-cli --session s network block -t Image -t Stylesheet -t Font \
-- screenshot https://heavy-site.com -o shot.png
# Mock an internal API while driving the page
pydoll-cli --session s network mock -p /api/me --status 200 --body fixture.json \
-- get https://app.compydoll-cli exposes the full pydoll feature set as subcommands. See pydoll-cli <command> --help for examples.
| Category | Commands |
|---|---|
| Navigation | get [URL] --wait-for SEL, source, text |
| Capture | screenshot, pdf, bundle |
| Interaction | click, type, eval, query, keyboard, mouse, scroll, upload |
| SPA waits | wait --selector|--network-idle|--url-contains|--page-event|--js|--stable-ids |
| Parallel | batch URL [URL...] (asyncio.gather across tabs) |
| Extraction | extract (Pydantic schema — Python file or JSON) |
| Network | request, har record, har replay, network logs, network watch |
| Interception | network block, network mock, network inject-header, network fail (Fetch wrap pattern) |
| Console | console logs (retroactive — Chrome replays the buffered history), console watch |
| Cookies & state | cookies get, cookies set, cookies clear |
| Stealth / evasion | cloudflare bypass, cloudflare auto-solve, humanized typing/clicking, --webrtc-leak-protection |
| Sessions | session start, session stop, session list, session info, session attach, session prune, session rm |
| Scripting | shell, run SCRIPT.py |
| Introspection | info, browsers |
| Flag | Meaning |
|---|---|
--browser {chrome,edge,wavebox,chromium} |
Which browser (default chrome). |
--browser-binary PATH |
Custom executable path. Overrides --browser detection. |
--headless / --no-headless |
Default --headless. |
--user-data-dir PATH |
Persistent profile directory. |
--incognito |
Launch incognito. |
--proxy URL |
scheme://user:pass@host:port. Credentials handled automatically. |
--proxy-insecure |
Append --ignore-certificate-errors. For authenticated proxies (Bright Data, etc.) that present an internal CA cert. |
--webrtc-leak-protection |
Enable pydoll WebRTC leak protection (recommended when proxying — WebRTC otherwise reveals the real IP). |
--page-load-state {complete,interactive} |
When to consider navigation complete. interactive returns on DOMContentLoaded (~2–5× faster on JS-heavy pages); complete (default) waits for full load. |
--user-agent STRING |
Override UA (Client Hints + navigator auto-synced). |
--accept-languages CSV |
e.g. en-US,en. |
--window-size WxH |
e.g. 1920x1080. |
--disable-images |
Skip image loading. |
--in-container |
Docker/CI-only: add --no-sandbox + --disable-dev-shm-usage. Skip on a normal desktop — the Chrome sandbox should stay on. |
-a, --arg TEXT |
Repeatable raw Chromium flag, e.g. -a --no-sandbox. |
--pref KEY=VAL |
Repeatable nested preference, profile.password_manager_enabled=false. |
--cdp-port PORT |
Fix the remote-debugging port (default: random). |
--connect WS_URL |
Attach to a running browser's WebSocket endpoint; don't launch one. |
--session NAME |
Reuse a persistent session (see session start). |
--tab INT / --tab-url URL |
Target a specific tab when using --session / --connect. |
--new-tab |
With --connect/--session: open a new tab (default context). |
--fresh |
With --connect/--session: open a new incognito context + tab; leaves existing tabs untouched. Ideal for driving your logged-in browser without disturbing it. |
--include-internal |
Count browser-internal targets (devtools://, chrome://) as tabs. Excluded by default so an open DevTools window can't be auto-selected instead of your app. Explicitly asking for one (--tab-url devtools://) always works. |
Session-start-only flags (on session start):
| Flag | Meaning |
|---|---|
--attach/--no-attach |
Attach to a running browser on --attach-port (default 9222) instead of launching one. On by default for --browser wavebox. |
--attach-port PORT |
CDP port of the running browser to attach to (default 9222). |
--share-profile |
With --attach: pin a tab in the running browser's default (logged-in) context instead of a fresh incognito context. session stop then leaves the pinned tab open by default (use --close-tab to close it). |
--url URL |
Navigate the pinned tab to this URL on startup. With --share-profile: if a tab matching this URL is already open, it's adopted as-is (no duplicate, no re-navigation). |
--tab-url SUBSTR |
With --share-profile only: adopt-only. Pin an already-open tab whose URL contains SUBSTR. Errors with exit 4 if no match. Mutually exclusive with --url. |
--startup-timeout SEC |
Seconds to wait for CDP readiness on a fresh launch (default 30). |
--timeout SECONDS |
Per-command timeout (default 30). |
--output {text,json} |
json for stable machine-readable output. |
-q, --quiet |
Suppress non-data output. |
--log-level LEVEL |
Python logging level (forwarded to pydoll). |
--log-file PATH |
Write pydoll logs to a file. |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic failure |
| 2 | CLI argument error (typer) |
| 3 | Operation timed out |
| 4 | Element not found |
| 5 | Browser launch failed |
| 6 | Session not running, or its pinned tab is gone |
| 7 | JavaScript exception in eval |
| 130 | Interrupted (Ctrl-C) |
See AGENTS.md for patterns, the stable JSON contract, and recommended workflows for letting an agent drive a persistent browser session across multiple tool invocations.
This repo ships a Claude Code skill at .claude/skills/pydoll-cli/ — a SKILL.md plus references/ files for situational topics (attached sessions/Wavebox, network interception) that Claude reads on demand. It teaches Claude the canonical session-based workflow, the stable JSON contract, and the real gotchas that trip up naive agents (tab-index instability, get reusing the current tab, extract --schema field syntax, etc.). The always-loaded body is kept lean so it only pays context when triggered.
Inside this repo it auto-loads at project scope with no action needed. To install it elsewhere, use the bundled command (the skill ships inside the wheel, so it works for every install method above):
# Default: project scope — install into the current repo (.claude/skills/pydoll-cli/).
# Run this from any project root to make the skill available in that repo only.
pydoll-cli install-skill
# User scope — make it available in every Claude Code session on your machine.
# Writes to ~/.claude/skills, or $CLAUDE_CONFIG_DIR/skills if you relocate the
# config tree (otherwise the skill lands where Claude Code never looks).
pydoll-cli install-skill --scope user # → <config dir>/skills/pydoll-cli/
# Custom location.
pydoll-cli install-skill --target ./.claude/skills
# Overwrite an older copy.
pydoll-cli install-skill --forceClaude Code follows the same scope hierarchy for skills as it does for plugins/settings — see Anthropic's skills and plugins docs. (For a single skill like this one, a plain skill is the right packaging; the plugin format is for bundles of skills + agents + hooks.)
Repo-clone alternatives (track in-repo edits, no reinstall needed):
# Symlink (edits/upgrades flow through automatically)
mkdir -p ~/.claude/skills
ln -s "$(pwd)/.claude/skills/pydoll-cli" ~/.claude/skills/pydoll-cli
# Or copy (independent snapshot; update manually on upgrades)
cp -r .claude/skills/pydoll-cli ~/.claude/skills/pydoll-cliVerify: start Claude Code and ask "what skills are available?" — pydoll-cli should be listed. Or run /pydoll-cli to invoke it directly.
The skill complements AGENTS.md: the skill is the compact in-context primer (always available, trigger-aware), AGENTS.md is the deep reference (only read when needed). The skill points at AGENTS.md for the long tail.
git clone https://github.com/dovidgef/pydoll-cli && cd pydoll-cli
uv sync --group dev
uv run pre-commit install # runs ruff format, ruff check, mypy, pytest on each commit
uv run pydoll-cli --help
uv run pytest # unit tests
uv run pytest -m integration # opt-in; launches a real browser
uv run ruff check . && uv run ruff format --check .Pre-commit mirrors the GitHub Actions CI checks (.pre-commit-config.yaml),
so a passing local commit means a passing CI run.
CI (.github/workflows/ci.yml) runs on every push and PR to master: ruff,
mypy, the unit suite on Python 3.10–3.13 plus macOS, and a packaging job that
builds the sdist/wheel and smoke-tests the installed console script. The
browser-backed integration tests are a separate manual-dispatch workflow
(.github/workflows/integration.yml) so a flaky real-Chrome run can never
block a PR.
There is no package index to publish to — a release is a tag, and users install
git+https://github.com/dovidgef/pydoll-cli@vX.Y.Z.
# 1. Bump src/pydoll_cli/__init__.py (__version__) — pyproject derives it.
# 2. Move the CHANGELOG "Unreleased" section under a [X.Y.Z] — date heading.
# CI fails if the version and the top CHANGELOG entry disagree.
git commit -am "chore: release X.Y.Z"
git tag -a vX.Y.Z -m "vX.Y.Z" && git push origin master --follow-tags