stealth-cli
v0.9.0
Published
Anti-detection browser CLI powered by Camoufox. Browse, search, scrape, and crawl the web with C++ level fingerprint spoofing.
Maintainers
Readme
Why
Headless Chrome gets fingerprinted. Playwright gets blocked. Stealth plugins become the fingerprint.
stealth-cli uses Camoufox — a Firefox fork that patches fingerprint generation at the C++ implementation level. No JavaScript shims, no wrappers, no tells. The browser reports spoofed values natively.
Wrap that in a developer-friendly CLI with 17 commands, and you get a tool that both humans and AI agents can use.
How it compares
| Approach | Detection Risk | Why |
|----------|:---:|------|
| Puppeteer + stealth plugin | 🔴 High | JS-level patches are detectable; Chromium TLS fingerprint is a giveaway |
| Playwright + custom args | 🟡 Medium | Better, but navigator.webdriver workarounds are fragile |
| undetected-chromedriver | 🟡 Medium | Patches Chrome binary, but still Chromium-based fingerprint |
| stealth-cli (Camoufox) | 🟢 Low | Firefox fork with C++ native spoofing; no JS shims to detect |
Install
npm install -g stealth-cliThe install fetches the Camoufox release this version is verified against (~300 MB, once;
scripts/fetch-camoufox.mjs, which the postinstall runs). Subsequent runs are instant.
git clone https://github.com/Youhai020616/stealth-browser.git stealth-browser
cd stealth-browser
npm ci # Installs workspaces + downloads Camoufox
npm start -- --help # Run from the repository root
cd packages/stealth-cli
npm link # Optional: make the stealth command globalQuick Start
stealth browse https://example.com # Visit a page
stealth screenshot https://example.com -o page.png # Screenshot
stealth search google "best coffee beans" -f json # Search Google
stealth extract https://example.com --links # Extract links
stealth crawl https://example.com -d 2 -l 50 -o out.jsonl # Crawl
stealth profile create work --preset us-laptop # Create a saved identity once
stealth open https://example.com --profile work # Human login flow
stealth interactive --url https://example.com # REPL modeHow Anti-Detection Works
stealth-cli (Node.js CLI)
└── camoufox-js (npm binding)
└── Camoufox (C++ Firefox fork)
└── Fingerprint spoofing at the native level| Fingerprint Vector | Approach |
|---|---|
| navigator.hardwareConcurrency | Spoofed in C++ |
| navigator.webdriver | Always false |
| WebGL renderer / vendor | Spoofed in C++ |
| AudioContext fingerprint | Spoofed in C++ |
| Canvas fingerprint | Spoofed in C++ |
| Screen geometry | Spoofed in C++ |
| WebRTC leak | Built-in protection |
| TLS fingerprint | Firefox native (not Chromium) |
No JavaScript shims. No detectable wrappers. The browser natively reports spoofed values.
Commands
Core (12)
| Command | Description |
|---------|-------------|
| browse <url> | Visit URL, print content (text/json/snapshot/markdown) |
| screenshot <url> | Screenshot (PNG/JPEG, full page, custom viewport; --annotate labels every interactive element with its ref) |
| search <engine> <query> | Search 14 engines with anti-detection |
| extract <url> | Extract links, images, meta, headings, CSS selectors |
| crawl <url> | Recursive crawling with depth/filter/delay control |
| open [url] | Headed browser for human authentication; exits when all windows close |
| interactive | Stdin-driven REPL with 20+ commands (goto, click, type, eval...) |
| pdf <url> | Save a rasterized screenshot-based PDF (Firefox/Camoufox) |
| batch <file> | Batch process URLs from file |
| monitor <url> | Watch for changes (price drops, stock alerts) |
| fingerprint | Check fingerprint & anti-detection status |
| serve | HTTP API server with Bearer token auth |
Management (5)
| Command | Description |
|---------|-------------|
| daemon start/stop/status | Background browser (~1s vs ~6s startup) |
| profile create/list/delete | Persistent identities (8 presets + random) |
| proxy add/list/test | Proxy pool with rotation & health checking |
| config set/get/list/reset | Global defaults (~/.stealth/config.json) |
| mcp | MCP server for Claude Desktop / Cursor |
Usage Examples
Browse
stealth browse https://example.com # Text output
stealth browse https://example.com -f json # JSON with metadata
stealth browse https://example.com -f snapshot # Accessibility tree
stealth browse https://example.com --humanize # Human behavior simulation
stealth profile create work --preset us-desktop # Create a saved identity once
stealth browse https://example.com --profile work # Reuse the saved identity
stealth browse https://example.com --proxy http://proxy:8080Search
Google uses a special anti-detection flow: visits homepage → types query with human-like timing → presses Enter.
stealth search google "web scraping tools" -f json # Auto-humanized
stealth search google "query" -f json --warmup # Visit random site first
stealth search duckduckgo "privacy browser" -f json
stealth search youtube "tutorial" -f json # Video metadata
stealth search github "camoufox" -f json # Repo results14 engines: google · bing · duckduckgo · youtube · github · amazon · reddit · wikipedia · twitter · linkedin · tiktok · stackoverflow · npmjs · yelp
Extract
stealth extract https://example.com --links # All links
stealth extract https://example.com --images # All images
stealth extract https://example.com --meta # Title, description, OG
stealth extract https://example.com --headers # h1-h6 headings
stealth extract https://example.com -s ".price" --all # CSS selector
stealth extract https://example.com -s "a" -a "href" --all # AttributesCrawl
stealth crawl https://example.com -d 2 -l 50 # Depth 2, max 50 pages
stealth crawl https://example.com -o results.jsonl # Save to file
stealth crawl https://example.com --include "blog" # URL regex filter
stealth crawl https://example.com --delay 2000 --humanize
stealth crawl https://example.com --proxy-rotate # Rotate per pageMonitor
stealth monitor https://shop.com/item -s ".price" -i 60 # Check every 60s
stealth monitor https://shop.com/item --contains "In Stock"
stealth monitor https://example.com --not-contains "Sold Out"Headed Browser for Human Authentication
# Create the identity once, then open a browser that lives until its windows close
stealth profile create work --preset us-laptop
stealth open https://example.com/login --profile work
# Named sessions can be checkpointed at the same time
stealth open --url https://example.com/login --profile work --session login-flowopen is always headed and always uses a direct browser, even when the daemon is running. It ignores stdin, checkpoints cookies every second, performs a final live save when the last page closes, and gracefully handles SIGHUP, SIGINT, and SIGTERM on POSIX systems. If the browser process exits before a final live capture, the latest durable checkpoint is retained and a warning is printed. --checkpoint-interval accepts integer values from 250 through 60000 ms (default: 1000).
Interactive REPL
stealth interactive --url https://example.com
stealth interactive --no-headless --profile work --session debugging
stealth> goto https://google.com
stealth> click "button.submit"
stealth> hclick "a.link" # Human-like click (bezier curve)
stealth> type "input[name=q]" hello
stealth> htype "input[name=q]" hello # Human-like typing (variable speed)
stealth> scroll down 3
stealth> text / snapshot / links # Inspect page
stealth> text markdown # Page as Markdown (headings, lists, links, tables)
stealth> screenshot page.png
stealth> screenshot labelled.png annotate # Ref boxes and labels drawn on the image
stealth> eval document.title
stealth> exitKey Features
Daemon Mode
Keep a browser alive in the background for instant command execution.
stealth daemon start # Start background browser
stealth browse https://example.com # ~1.2s (vs ~6s cold start)
stealth daemon stop # Shut down (auto-stops after 5min idle)Browser Profiles
Persistent identities with unique fingerprints. Cookies auto-save between sessions.
stealth profile create work --preset us-desktop
stealth profile create rand1 --random
stealth browse https://example.com --profile work
# → Fingerprint: Windows, en-US, America/New_York
# Human-assisted login; cookies are checkpointed while the browser is open
stealth open https://example.com/login --profile work8 presets: us-desktop · us-laptop · uk-desktop · de-desktop · jp-desktop · cn-desktop · mobile-ios · mobile-android
Session Persistence
# Link a named session to the existing work profile
stealth browse https://example.com --session my-task --profile work
# → Cookies + URL + history saved
# Session-only open restores the linked work profile and the saved URL
stealth open --session my-task
# An explicit initial URL skips the session's saved URL
stealth open https://other.com --session my-taskWhen --profile and --session are combined, the profile is the canonical cookie source and the session restores URL/history metadata. A session already linked to a different profile is rejected instead of merging two identities. When only --session is supplied, a linked profile is restored automatically. For open and interactive, an explicit initial URL takes precedence: stealth-cli skips the saved session URL before navigating to the requested URL.
Behavior/security change: Profile and session names may contain only ASCII letters, numbers, underscores, and hyphens (
[A-Za-z0-9_-]+) and use lowercase canonical identities. Windows device basenames such asCON,NUL,COM1, andLPT1are rejected on every platform. Existing mixed-case filenames are resolved case-insensitively. Older releases replaced unsupported characters with underscores in the filename while retaining the original name in JSON; for state previously requested aswork.prod, reuse the sanitized basenamework_prod.stealth profile listshows reusable profile basenames. There is no session-list CLI command, so inspect the filenames in$STEALTH_HOME/sessions(default~/.stealth/sessions) when migrating an older session. Compatible legacy metadata is canonicalized and rewritten on the next successful state save; files are not renamed automatically. If a session links to a profile that no longer exists, startup fails before the browser launches instead of silently falling back to a different identity.
Named profile and session browser state is single-writer. Browser lifetimes and standalone create/save/delete operations all participate in the same lock protocol, so concurrent mutation fails instead of mixing identities. Lock files are never removed automatically after a crash: when the recorded owner is no longer running, stealth-cli prints the exact lock path and requires explicit removal after you confirm no process still owns that state. Profile and session JSON files contain authentication material; writes are atomic. On POSIX platforms, directories and files are enforced to owner-only modes (0700 and 0600). Windows does not provide equivalent POSIX mode-bit enforcement, so protect STEALTH_HOME with user-only Windows ACLs and do not place it in a shared directory.
By default, profiles, sessions, and their locks live under ~/.stealth. Set STEALTH_HOME to relocate those paths together. The configured state root and child paths must not be symlinks. Configuration (config.json), proxy-pool (proxies.json), and daemon socket/PID paths remain under ~/.stealth in the current source; config and proxy-pool files are stored with the same owner-only POSIX directory/file modes (0700/0600). Windows users must protect these paths with user-only ACLs. A hard browser crash can only preserve state captured by the most recent checkpoint; open defaults to a one-second interval.
Proxy Pool
stealth proxy add http://proxy1:8080 --label us --region US
stealth proxy add http://proxy2:8080 --label eu --region EU
stealth proxy test # Health check
stealth browse https://example.com --proxy-rotate # Auto-rotateGeoIP: Camoufox auto-matches locale, timezone, and geolocation to proxy exit IP.
Humanize Mode
Simulate human behavior patterns to avoid behavioral detection:
stealth browse https://example.com --humanize
stealth search google "query" --humanize --warmup- Gaussian-distributed delays between actions
- Bézier-curve mouse movements
- Variable typing speed
- Random scroll patterns
Global Configuration
Set defaults so you don't repeat flags:
stealth config set locale zh-CN
stealth config set humanize true
stealth config set retries 3
stealth config set format json
stealth config listAll core commands respect global config. CLI flags always override.
Pipe-Friendly
stdout = data, stderr = status. Compose with Unix tools:
stealth browse https://api.example.com -f json | jq '.title'
stealth search google "query" -f json | jq '.results[].url'
stealth extract https://example.com --links -f json | jq '.data[].url'Integrations
HTTP API Server
Run stealth-cli as a service for programmatic access:
stealth serve --port 9377
# → Prints auto-generated API token on startup
curl localhost:9377/health # No auth
curl -X POST localhost:9377/tabs \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com"}' # Create tab
curl localhost:9377/tabs/tab-1/text -H 'Authorization: Bearer <token>' # Get textOptions: --token <custom> · --no-auth (localhost only) · --host 0.0.0.0
Endpoints: /health · /tabs (POST/GET) · /tabs/:id/navigate · /tabs/:id/text · /tabs/:id/snapshot · /tabs/:id/screenshot · /tabs/:id/click · /tabs/:id/type · /tabs/:id/evaluate · /tabs/:id (DELETE) · /shutdown
MCP Server (Claude Desktop / Cursor)
Add stealth browsing capabilities to your AI coding assistant:
{
"mcpServers": {
"stealth": {
"command": "stealth",
"args": ["mcp"]
}
}
}Legacy tools: stealth_browse (format: text, markdown, snapshot) · stealth_screenshot (annotate draws ref boxes and labels) · stealth_search · stealth_extract · stealth_evaluate · stealth_snapshot, plus one stealth_<action> tool per registered agent-facing action (stealth_observe, stealth_click, stealth_type, stealth_text, stealth_find, …)
Perception snapshot
stealth_snapshot and stealth interactive → a11y return the perception snapshot:
URL: https://shop.example/cart
Title: Your cart
Scroll: 0px above, 1840px below
Interactive elements: 3
Occluded: 2 — marked [occluded], no ref
New since last snapshot: 1 — marked [new]
- generic [active]:
- button "Checkout" [occluded]
- link "Continue shopping" [occluded]
- dialog "Cookies":
- text: We use cookies.
- button "Accept" [ref=e7] [new]
- iframe:
- textbox "Card number" [ref=f1e3]
- button "Pay" [ref=f1e4]- Interactive refs are checked at five points, including through every parent iframe. Covered elements are shown as
[occluded]without a ref in the tree. Offscreen refs remain available for scrolling; unchecked refs remain whenpageInfo.occlusionCheckispartial. Clicking repeats the same checks after scrolling and refuses covered targets (passforce: trueto override). Frame geometry supports 2D transforms; 3D/perspective and motion-path frame transforms are refused by the guard. - A vertical scroll container keeps its ref and is marked
[scrollable](for example- generic "Items" [ref=e3] [scrollable]:), soscroll { ref, delta_y }moves that region instead of the page;scrollables[]names the sameref. Only main-frame containers that page info measures are matched, within a short bounded lookup that a page without one never runs. - Refs are stable within each document;
[new]marks elements that appeared since your last snapshot (autocomplete suggestions, dialogs), including reused ref numbers after a child iframe navigates. A main-page navigation resets the baseline. - Open shadow DOM and iframes are included. Child-frame refs carry a frame prefix (
f1e4) and work withstealth_click/stealth_typeunchanged. - Nothing is written into the page: no marker attributes, no globals. Refs live in Playwright's isolated world.
stealth browse -f snapshot, the interactive snapshot command, and HTTP /snapshot
keep the plain accessibility-tree format without refs or perception metadata. Use
getA11ySnapshot() / buildA11yTree() in the SDK or HTTP /a11y-snapshot for perception.
SDK (Library Mode)
Use stealth-cli programmatically in your Node.js applications. The work profile must be created first, either with stealth profile create work --preset us-desktop or the SDK's createProfile('work', { preset: 'us-desktop' }) API.
import {
launchBrowser,
closeBrowser,
checkpointBrowserState,
navigate,
getTextContent,
getMarkdown,
takeScreenshot,
} from 'stealth-cli';
const handle = await launchBrowser({ profile: 'work', humanize: true });
let operationError = null;
try {
await navigate(handle, 'https://example.com');
const text = await getTextContent(handle);
console.log(text);
const { text: markdown, truncated } = await getMarkdown(handle, { maxChars: 20000 }); // structure kept
const { boxes } = await takeScreenshot(handle, { path: 'labelled.png', annotate: true }); // ref labels drawn on the image
await checkpointBrowserState(handle); // Optional durable checkpoint while still open
} catch (error) {
operationError = error;
}
let closeError = null;
try {
await closeBrowser(handle, { strict: true });
} catch (error) {
closeError = error;
}
if (operationError && closeError) {
throw new AggregateError([operationError, closeError], 'Browser operation and cleanup both failed');
}
if (operationError) throw operationError;
if (closeError) throw closeError;closeBrowser(handle) is best-effort by default: it attempts persistence and cleanup, then returns { persistence, persistenceError, cleanupErrors } instead of throwing for those failures. Inspect both error fields if you use the default mode. With { strict: true }, cleanup is still attempted, then an exported PersistenceError or BrowserCleanupError is thrown when work remains incomplete. Persistence is captured at most once; calling closeBrowser(handle) again retries only unfinished context, browser, or state-lock cleanup and never recaptures persistence. Fix the reported storage issue before repeating an authentication flow whose final persistence failed, and retry the same handle only for unfinished cleanup before reusing its named profile/session.
A failed launchBrowser() performs two bounded rollback attempts. If its error still reports incomplete browser cleanup, retain that original error and call retryBrowserLaunchCleanup(error, { strict: true }); the SDK keeps the rollback handle and state lease private and removes the retry capability only after cleanup completes. If profile/session lock acquisition or a standalone state operation reports incomplete state-lock cleanup, retain that exact error object and call retryStateLockCleanup(error); cloned or serialized errors intentionally do not carry the private recovery capability.
Use checkpointBrowserState(handle) for manual mid-run persistence on a handle launched with a named profile or session; it reuses the private browser-owned lease. The lower-level captureSession(name, context, page) API remains suitable for independently managed contexts, but it cannot acquire a session name already owned by a live launchBrowser() handle.
A successful close result or checkpointBrowserState(handle) result can contain cookie values and the raw last URL, and error objects retain non-enumerable diagnostic fields such as .cause or .url. Do not log, serialize, or return browser handles, checkpoint results, or raw close results across trust boundaries. Store STEALTH_HOME as sensitive authentication state. Error toJSON() output is redacted for routine logging, but the in-memory objects must still be treated as sensitive.
Atomic state writes hold a unique destination-scoped .claim from admission through publish or rollback sync. They fail closed when a prior process leaves a strict .claim, .tmp, or .rollback artifact. stealth-cli never auto-removes cross-process artifacts: verify that no live process owns the artifact, inspect it, and remove only the exact owner-only path reported by the command before retrying. On POSIX, sensitive path ancestors must be controlled by the current user or root and must not be non-sticky group/other-writable; user-controlled ancestor symlinks are rejected, while system-owned aliases are accepted only after their canonical target chain passes the same checks. This protocol serializes cooperative stealth-cli writers on a coherent local filesystem; owner-only files do not sandbox authentication state from hostile code already running as the same OS user.
Error Handling
stealth-cli provides structured errors with contextual hints:
✖ Failed to navigate to https://example.com
Hint: Page load timed out. Try --wait <ms> or --retries <n>
✖ Google detected automation and blocked the request
Hint: Try: --proxy <proxy>, --warmup, --humanize, or use a different engine
✖ Profile "work" not found
Hint: Create with: stealth profile create workExit codes: 0 success · 1 general error · 2 invalid arguments · 3 browser launch · 4 navigation/blocked · 5 extraction · 6 timeout · 7 proxy · 8 profile/session/persistence · 129 SIGHUP · 130 SIGINT · 143 SIGTERM
Common Options
Option availability varies by command; run stealth <command> --help for the exact set.
| Option | Description |
|--------|-------------|
| --proxy <url> | Use proxy server |
| --proxy-rotate | Rotate through proxy pool |
| --profile <name> | Use saved browser identity |
| --session <name> | Persist/restore browsing session |
| --cookies <file> | Import Netscape-format cookie file |
| --humanize | Simulate human behavior |
| --retries <n> | Max retries on failure |
| --no-headless | Show browser window on commands that default to headless |
| --checkpoint-interval <ms> | open authentication-state checkpoint interval (250–60000; default: 1000) |
| -f, --format | Output format: text, json, jsonl, snapshot, markdown |
Project Stats
Version: 0.9.0
Commands: 17
Tests: 742 passing (57 test files)
Source: 17,519 lines (78 JavaScript files under `src/`)
Extractors: 6 (Google, Bing, DuckDuckGo, YouTube, GitHub, generic)
Presets: 8 browser profiles
Engine: Camoufox (C++ Firefox fork)
License: MITContributing
Start with SPEC.md — what the product is and what it declines — and
AGENTS.md — the hard rules, the documentation map, and the gate before a push.
Contributions welcome! Some areas where help is appreciated:
- New extractors — Add structured parsing for more search engines/sites
- Profile presets — More realistic browser fingerprint configurations
- Bug reports — Especially sites that still detect stealth-cli
- Documentation — Usage guides, tutorials, examples
Please open an issue to discuss larger changes before submitting a PR.
Star History
🔗 Ecosystem
| Project | Description | |---------|-------------| | AgentMind | Self-learning memory system for AI agents | | stealth-x | Anti-detection X/Twitter automation (built on stealth-cli) | | dy-cli | Douyin/TikTok CLI | | xiaohongshu | Xiaohongshu automation | | freepost | AI social media management |
Acknowledgments
- Camoufox — The Firefox fork that makes this possible
- Playwright — Browser automation framework
License
Native video through the SDK
Install the video encoder with npx playwright-core install ffmpeg. Pass
recordVideo: { dir: '/private/output', size: { width: 1280, height: 720 } }
to launchBrowser(). This opts into a direct context even if a daemon exists;
no action annotations are injected into pages. Keep const video = handle.page.video(),
perform browser work, await closeBrowser(handle, { strict: true }), then read
await video.path(). Output is WebM. Each context page has its own video;
callers own storage permissions, file retention and cleanup of the output directory.
Recording is off by default; the CLI/daemon do not expose a recording flag.
A synthetic click that triggers a matching attachment reports
navigationStatus: "downloaded". This confirms the download started, not that
its bytes were saved; use Playwright's download completion API separately.
